Skip to content

refactor: run query-side computation as Planner physical DAGs - #798

Open
zzylol wants to merge 4 commits into
refactor/precompute-dag-heapsfrom
refactor/query-side-planner-compile
Open

zzylol wants to merge 4 commits into
refactor/precompute-dag-heapsfrom
refactor/query-side-planner-compile

Conversation

@zzylol

@zzylol zzylol commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #800.

Why

Planner's physical_planner::compile now covers the query-side computation the backend still lowered itself. The backend re-parsed PromQL text to recover operators, kept its own operator graph (QueryTimeOperator Binary/Aggregate/Sort/Limit/Temporal/Subquery and QueryPlanNode Scalar/Binary/ReduceSum), translated it to label-map operators, and evaluated subquery grids in the data plane. That is a second lowering of the same computation.

What

  • The backend lowers only stored-state readouts (ReadMaterialization, ExactReadout, SummaryEstimate, SummaryMerge, current series). The query-time region above them is compiled once by physical_planner::compile, with the readouts as input contracts, into one PhysicalFragment.
  • A root that Planner cannot compile over readouts keeps no state. The whole query goes to the exact engine (ExactFallback, forwarded unchanged). Queries that Planner keeps pre-ASAP are handled the same way.
  • Deleted: query_time::Lower, compile_logical, the query_time_nodes/selected_* text witnesses, physical_values.rs, the DagCompiler Binary/Logical/Scalar/ReduceSum arms, the computation variants of QueryTimeOperator and QueryPlanNode, and the data-plane subquery grid and logical evaluation. Counter and range-maximum materializations are now identified from the selected node.
  • Raw-series programs (Physical, from feat: bind query-time raw series inputs to the Prometheus endpoint #792) record whether PromQL drops __name__ from the result. The adapter removes it when decoding series identities. Planner keeps it.

Before / After

sum by (job) (rate(errors_total[5m])) / sum by (job) (rate(requests_total[5m]))

Installed query nodes
Before Frag[VectorBinary, Project×3, Aggregate×2] built by physical_values from re-parsed Logical Aggregate/Binary nodes over two ExactReadout
After two ExactReadout → one Planner PhysicalFragment[Join, Project, SummaryBuild, Readout ×2]

topk(2, sum by (job) (rate(m[1h]))) with an unbindable child: before, Prometheus computed the child and the backend ran topk. After, Prometheus answers the whole query.

Startup on the warm test (collector_free_profile_serves_complete_matrix_and_falls_back_exactly, three runs each): 3.32/3.27/3.24 s before, 3.14/3.09/3.13 s after.

Behaviour differences

  • Per-series arithmetic over stored readouts is forwarded whole. Planner's Binary does not yet match per-series rows (coverage Remaining 3). Affected queries were warm before and are now exact: avg_over_time(x[5m]) (sum/count), rate(a[5m]) / rate(b[5m]), rate(x[5m]) * 2, and sum_over_time(a[1m]) + sum_over_time(a{job="x"}[1m]). Grouped forms such as sum(...) / sum(...) and sum(...) * 2 stay warm. The issue 701/702 tests are updated to match.
  • A computation over an exact PromQL subtree, such as topk over an unbindable child, is forwarded whole instead of being split between Prometheus and the backend.
  • Some roots failed to compile before and are now forwarded: -sum(rate(x[5m])) now runs warm, and min by (job) (...) and mixed MetricsQL counter queries are now exact.
  • An ephemeral state is chosen only when a raw-series program compiles. The per-leaf "externalizable" path is gone.
  • A query that compiled state but cannot bind it now fails its candidate, where before it was mixed or errored. Its state is never maintained unread. A query with no compiled state is forwarded whole in every mode.
  • An instant count, which has no local exact readout, is never an input to a Planner fragment.
  • Raw-series results no longer carry __name__ where PromQL drops it.
  • Persisted plans that contain the removed variants no longer deserialize.

Remaining backend computation

  • SummaryEstimate / ExactReadout / SummaryMerge computation in post_asap_readout.rs (coverage row 16 is Partial; rows 15 and 17 need Planner-typed stored state bindings).
  • Current-series Sum/Count/Average/Quantile/TopK readouts in storage_engines/sketch_db/current_series.rs are moved in the stacked refactor: compute current-series readouts as Planner physical programs #799.
  • Uncovered shapes are forwarded: histogram_quantile, comparisons and set operators, group_left/right, per-series Binary over readouts, and implicit subquery resolution.
  • The control plane no longer emits ExactSubquery/CandidateExactSubquery. Their data-plane runtime (exact_subqueries.rs, which re-parses PromQL to inject candidate matchers) is dead for new plans and can be removed in a follow-up.
  • The metric-name rule cannot tell bool comparisons apart, because the IR does not record bool. Planner compiles no comparison yet.

Validation

  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets --locked -- -D warnings
  • cargo test --workspace --locked --lib, cargo test -p control_plane --locked --tests
  • cargo test -p data_plane --locked --test asapquery_compatibility_process_e2e -- --test-threads=1 (26 passed)
  • New tests: a grouped ratio and a topk execute as one Planner fragment over readouts. The o11y corpus uses only Planner fragments or whole forwarding. Per-series averages, topk over exact children and mixed MetricsQL counters forward whole. Raw-series results drop __name__. The retired operator variants are rejected.
  • An independent review agent, which did not write the code, found five issues. Forwarding happened after placement and left state unread. Non-mixed deployments failed on unselected states. Pruned joins below the root were not guarded. Instant counts reached fragment inputs. The metric-name rule had gaps. All five are fixed with regression tests in the follow-up commits, and a re-review found no regressions.

🤖 Generated with Claude Code

zzylol and others added 4 commits September 30, 2026 08:55
The backend re-lowered query-time computation itself: it re-parsed PromQL
text to recover each operator (query_time::Lower, query_time_nodes and the
selected_* witnesses), built QueryTimeOperator Binary/Aggregate/Sort/Limit/
Temporal/Subquery nodes and QueryPlanNode Scalar/Binary/ReduceSum, and
translated them into label-map operators (physical_values.rs). The data
plane evaluated subquery grids itself.

Now the backend lowers only stored-state readouts. The query-time region
above them is compiled once by physical_planner::compile, with the readouts
as input contracts, into one PhysicalFragment. A root Planner cannot compile
over readouts (per-series binaries, comparisons, histogram_quantile, exact
selectors under computation) keeps no state and is forwarded whole as
ExactFallback, as is any query Planner keeps pre-ASAP.

Counter and range-maximum materializations are identified from the
selected node, not from query text. The removed operator variants no
longer deserialize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Planner keeps __name__ in the series identity it computes, so a raw-series
program for quantile_over_time(0.5, m[5m]) answered with __name__="m",
which Prometheus drops. The compiler records on the Physical node whether
the query result keeps the metric name (only a selector, through ordering,
selection, subqueries and last_over_time does), and the adapter removes it
when decoding series identities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…out checks

Review follow-ups:
- A query whose readouts have no maintained state is forwarded whole in
  every mode; a query that compiled state but cannot bind it fails the
  candidate instead of maintaining that state unread. Previously a
  non-mixed deployment failed to compile, e.g. a ratio of two grouped
  rates whose states were not selected.
- Pruned joins read both sides as stored state; a pruned join below a
  region root is not compiled, since its contract binds only at the root.
- An instant count, which has no local exact readout, is not an input to
  a Planner fragment.
- Range-maximum states require a named metric.
- first_over_time, filters, relabeling and limitk keep __name__; series that
  collide once it is dropped are rejected, as PromQL does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The non-mixed ratio test now requires forwarding with no maintained state,
a binding failure names its query in the candidate error, and the metric
name follows the left side of and/unless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzylol
zzylol force-pushed the refactor/query-side-planner-compile branch from 0c9b78d to c1becfb Compare September 30, 2026 09:19
zzylol added a commit that referenced this pull request Sep 30, 2026
Restores the issue 701/702 warm avg_over_time assertions and the
finite-division overflow test that #798 rewrote, and adds control-plane
acceptance tests. These fail at the current Planner pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzylol
zzylol changed the base branch from feat/planspace-compile-once to refactor/precompute-dag-heaps September 30, 2026 09:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant