diff --git a/promql/engine.go b/promql/engine.go index a6b3cc723..34ea2a3e9 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -128,6 +128,8 @@ type Query interface { type QueryOpts struct { // Enables recording per-step statistics if the engine has it enabled as well. Disabled by default. EnablePerStepStats bool + // Lookback delta duration for this query. + LookbackDelta time.Duration } // query implements the Query interface. @@ -437,11 +439,17 @@ func (ng *Engine) newQuery(q storage.Queryable, opts *QueryOpts, expr parser.Exp opts = &QueryOpts{} } + lookbackDelta := opts.LookbackDelta + if lookbackDelta <= 0 { + lookbackDelta = ng.lookbackDelta + } + es := &parser.EvalStmt{ - Expr: PreprocessExpr(expr, start, end), - Start: start, - End: end, - Interval: interval, + Expr: PreprocessExpr(expr, start, end), + Start: start, + End: end, + Interval: interval, + LookbackDelta: lookbackDelta, } qry := &query{ stmt: es, @@ -636,7 +644,7 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval ctx: ctxInnerEval, maxSamples: ng.maxSamplesPerQuery, logger: ng.logger, - lookbackDelta: ng.lookbackDelta, + lookbackDelta: s.LookbackDelta, samplesStats: query.sampleStats, noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn, } @@ -688,7 +696,7 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval ctx: ctxInnerEval, maxSamples: ng.maxSamplesPerQuery, logger: ng.logger, - lookbackDelta: ng.lookbackDelta, + lookbackDelta: s.LookbackDelta, samplesStats: query.sampleStats, noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn, } @@ -801,7 +809,7 @@ func (ng *Engine) getTimeRangesForSelector(s *parser.EvalStmt, n *parser.VectorS } if evalRange == 0 { - start = start - durationMilliseconds(ng.lookbackDelta) + start = start - durationMilliseconds(s.LookbackDelta) } else { // For all matrix queries we want to ensure that we have (end-start) + range selected // this way we have `range` data before the start time diff --git a/promql/engine_test.go b/promql/engine_test.go index e71f7a7a9..9784ac22a 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -3120,3 +3120,96 @@ func TestRangeQuery(t *testing.T) { }) } } + +func TestQueryLookbackDelta(t *testing.T) { + var ( + load = `load 5m +metric 0 1 2 +` + query = "metric" + lastDatapointTs = time.Unix(600, 0) + ) + + cases := []struct { + name string + ts time.Time + engineLookback, queryLookback time.Duration + expectSamples bool + }{ + { + name: "default lookback delta", + ts: lastDatapointTs.Add(defaultLookbackDelta), + expectSamples: true, + }, + { + name: "outside default lookback delta", + ts: lastDatapointTs.Add(defaultLookbackDelta + time.Millisecond), + expectSamples: false, + }, + { + name: "custom engine lookback delta", + ts: lastDatapointTs.Add(10 * time.Minute), + engineLookback: 10 * time.Minute, + expectSamples: true, + }, + { + name: "outside custom engine lookback delta", + ts: lastDatapointTs.Add(10*time.Minute + time.Millisecond), + engineLookback: 10 * time.Minute, + expectSamples: false, + }, + { + name: "custom query lookback delta", + ts: lastDatapointTs.Add(20 * time.Minute), + engineLookback: 10 * time.Minute, + queryLookback: 20 * time.Minute, + expectSamples: true, + }, + { + name: "outside custom query lookback delta", + ts: lastDatapointTs.Add(20*time.Minute + time.Millisecond), + engineLookback: 10 * time.Minute, + queryLookback: 20 * time.Minute, + expectSamples: false, + }, + { + name: "negative custom query lookback delta", + ts: lastDatapointTs.Add(20 * time.Minute), + engineLookback: -10 * time.Minute, + queryLookback: 20 * time.Minute, + expectSamples: true, + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + test, err := NewTest(t, load) + require.NoError(t, err) + defer test.Close() + + err = test.Run() + require.NoError(t, err) + + eng := test.QueryEngine() + if c.engineLookback != 0 { + eng.lookbackDelta = c.engineLookback + } + opts := &QueryOpts{ + LookbackDelta: c.queryLookback, + } + qry, err := eng.NewInstantQuery(test.Queryable(), opts, query, c.ts) + require.NoError(t, err) + + res := qry.Exec(test.Context()) + require.NoError(t, res.Err) + vec, ok := res.Value.(Vector) + require.True(t, ok) + if c.expectSamples { + require.NotEmpty(t, vec) + } else { + require.Empty(t, vec) + } + }) + } +} diff --git a/promql/parser/ast.go b/promql/parser/ast.go index 89e6fb6da..7ae7cb112 100644 --- a/promql/parser/ast.go +++ b/promql/parser/ast.go @@ -68,6 +68,8 @@ type EvalStmt struct { Start, End time.Time // Time between two evaluated instants for the range [Start:End]. Interval time.Duration + // Lookback delta to use for this evaluation. + LookbackDelta time.Duration } func (*EvalStmt) PromQLStmt() {}