Summary
The PromQL planning path carries two window concepts:
- Declared lookback:
time_selection.lookback, which becomes PlanningQuery.window_secs and InstantExecution.lookback_ms.
- PromQL range selector: each leaf's
[range], which becomes the per-state window_secs and MaterializationBinding.readout_lookback_ms.
Wherever a leaf has a range, the range wins for the stored window, validation, retention and most serving. Lookback only really matters for (a) rangeless states, where it silently becomes the stored window, and (b) one serving path, where using it gives wrong results (see bug below). ASAPPlanner (ca7546d) only checks lookback.is_some() and never uses the value.
Everything here was found by reading the code; nothing was run.
Bug: /query_range reads the declared lookback instead of the range
For an entry with no Logical nodes (e.g. sum(sum_over_time(a[10m])) with a 60s lookback):
engine.rs:661-699 calls live_serve::serve_range_steps_from_query_plan, which calls execute_query_plan_instant.
- That computes
t0 = now - entry.instant.lookback_ms (post_asap_readout.rs:111-115), so it reads 1m of panes for a 10m query.
- Instant queries and range queries over
Logical entries overwrite instant.lookback_ms with the binding's readout_lookback_ms (engine.rs:291-305), so they are unaffected.
QueryPlan::validate only rejects readout_lookback_ms == 0 (crates/asap_types/src/query_plan.rs:431), so nothing catches the mismatch.
- The compiler tests that build this shape:
control_plane/src/physical/compiler.rs:5424-5451, :5387-5420.
Cases
| Case |
Example |
Lookback vs range |
What happens today |
| No range, exact |
sum by (job) (data) |
No [range], so only the lookback exists (1s in fixtures) |
The compiler won't store state for a bare scan, so Prometheus answers it. The lookback is computed but ends up unused. |
| No range, sketch |
quantile by (job) (0.5, data) |
No [range], so the lookback becomes the sketch's window |
The state gets a window from the lookback but no read-back range. Serving would reject it ("requires one explicit positive window"). The lookback quietly decides behavior here. |
| Multiple ranges |
sum(sum_over_time(a[1m])) / sum(sum_over_time(b[5m])) |
Two ranges, one lookback (at most one of them can match) |
Works. Each range gets its own state, and serving uses each state's own range. The lookback is ignored. |
| Single range ≠ lookback |
sum(sum_over_time(a[10m])), lookback 60s |
They conflict |
Compiles fine, and instant queries are correct. /query_range reads only 60s of data for a 10m query (the bug above). |
| Subquery |
avg_over_time((sum(m))[6h:]) |
The subquery's [6h:] range isn't picked up, so the lookback fills in |
The inner part isn't stored as state. How much data to keep comes from the subquery node itself, so the lookback has no effect. |
offset |
sum_over_time(m[1m] offset 1h) |
The range exists but the offset shifts it |
The compiler rejects stored state for offset queries, so Prometheus answers it. |
| HTTP path |
Any query sent to compile-and-publish |
The client sends window_secs directly |
If the client's window candidates don't match the query's actual range: hybrid mode silently drops the state and Prometheus answers the query; non-hybrid mode fails to compile. |
Only two rows depend on the lookback value itself: "No range, sketch" (it becomes the window) and "Single range ≠ lookback" (the bug). In every other row the lookback is redundant or ignored.
Checked-in fixtures (docs/examples/*.json, docs/examples/workloads/*.json) always use lookback = max range, or 1s when there is no range (generate.py:31: "lookback = scrape interval (1s)"). Mismatches only appear in Rust tests that reuse the 60s fixture.
Two concepts mixed together
- Dashboard time span: Planner
TimeSelection ("concrete event-time interval selected by a query"), ClickHouse end - start (control_plane/src/clickhouse.rs:266, 517).
- PromQL range selector: the semantic window of a leaf.
Questions
- Is the intended meaning of
time_selection.lookback the dashboard time span or the PromQL range? If it's the time span, should it affect retention or cost at all?
- What window should a rangeless sketch state (
quantile(0.5, m)) use?
- Does the HTTP
window_secs have any caller that relies on it differing from the range?
Summary
The PromQL planning path carries two window concepts:
time_selection.lookback, which becomesPlanningQuery.window_secsandInstantExecution.lookback_ms.[range], which becomes the per-statewindow_secsandMaterializationBinding.readout_lookback_ms.Wherever a leaf has a range, the range wins for the stored window, validation, retention and most serving. Lookback only really matters for (a) rangeless states, where it silently becomes the stored window, and (b) one serving path, where using it gives wrong results (see bug below). ASAPPlanner (
ca7546d) only checkslookback.is_some()and never uses the value.Everything here was found by reading the code; nothing was run.
Bug:
/query_rangereads the declared lookback instead of the rangeFor an entry with no
Logicalnodes (e.g.sum(sum_over_time(a[10m]))with a 60s lookback):engine.rs:661-699callslive_serve::serve_range_steps_from_query_plan, which callsexecute_query_plan_instant.t0 = now - entry.instant.lookback_ms(post_asap_readout.rs:111-115), so it reads 1m of panes for a 10m query.Logicalentries overwriteinstant.lookback_mswith the binding'sreadout_lookback_ms(engine.rs:291-305), so they are unaffected.QueryPlan::validateonly rejectsreadout_lookback_ms == 0(crates/asap_types/src/query_plan.rs:431), so nothing catches the mismatch.control_plane/src/physical/compiler.rs:5424-5451,:5387-5420.Cases
sum by (job) (data)[range], so only the lookback exists (1s in fixtures)quantile by (job) (0.5, data)[range], so the lookback becomes the sketch's windowsum(sum_over_time(a[1m])) / sum(sum_over_time(b[5m]))sum(sum_over_time(a[10m])), lookback 60s/query_rangereads only 60s of data for a 10m query (the bug above).avg_over_time((sum(m))[6h:])[6h:]range isn't picked up, so the lookback fills inoffsetsum_over_time(m[1m] offset 1h)compile-and-publishwindow_secsdirectlyOnly two rows depend on the lookback value itself: "No range, sketch" (it becomes the window) and "Single range ≠ lookback" (the bug). In every other row the lookback is redundant or ignored.
Checked-in fixtures (
docs/examples/*.json,docs/examples/workloads/*.json) always use lookback = max range, or 1s when there is no range (generate.py:31: "lookback = scrape interval (1s)"). Mismatches only appear in Rust tests that reuse the 60s fixture.Two concepts mixed together
TimeSelection("concrete event-time interval selected by a query"), ClickHouseend - start(control_plane/src/clickhouse.rs:266, 517).Questions
time_selection.lookbackthe dashboard time span or the PromQL range? If it's the time span, should it affect retention or cost at all?quantile(0.5, m)) use?window_secshave any caller that relies on it differing from the range?