Repository navigation
feat(planner): plan_installable, the one call a deployment runtime makes (#749) - #755
Merged
Merged
Conversation
This was referenced Oct 18, 2026
zzylol
force-pushed
the
feat/runtime-handoff-749
branch
2 times, most recently
from
October 18, 2026 17:58
7a1b4ad to
3246976
Compare
zzylol
force-pushed
the
feat/runtime-handoff-749
branch
3 times, most recently
from
October 18, 2026 21:55
c021915 to
bf1bb85
Compare
PlanOutput::physical_asap_dag's doc said every node is exported at query time, so a runtime planning Example 4b without raw data could not trust the export and read Stage 3's plan through run_stage1_to_stage3 instead. The export already keeps each node's timing and materialization (the timing pass honors a timing already written); only untimed nodes take the default. The doc now says so, and a facade test checks the export of Example 4b equals Stage 3's selected plan, ingestion-time panes included.
A deployment fills DeploymentCapabilities.supported_summaries, but had no list from the executor, so it declared every family the Planner builds and a plan with an unimplemented one failed only when the runtime bound it. supported_summary_families() returns each sketch family the factory builds per instance with the readouts an evaluation answers from it: plain Count Sketch (built, never read) and Theta (not built) are absent, Count-Min answers its total (its item frequency comes only from a Hydra grid, which Stage 2 does not produce yet), and the heap families answer top-k. A test checks every (family, readout) pair against create_planner_accumulator and the evaluation validators. SupportedSummary moves to asap-types beside StatisticKind so the executor can return it; asap-logical-asap-optimization re-exports it.
A runtime matched Stage 2 candidate ids (L<n> to P<n>) and repeated Stage 3's sizing to get its correctness reference, the selected plan with nothing materialized. PhysicalASAPDAG::with_every_node_at_query_time rebuilds the DAG's IR untimed, applies the default materialization and re-exports: the same operators, summaries and parameters, every node at query time. RawSampleWindow names a window of raw samples a runtime feeds the DAG: TimeRange(width) <- TimeShift(offset)* <- Scan, with bounds_ms(T). Two workload-level helpers support the installable plan: apply_materialization_timings_to_roots (the loop PlanOutput::physical_asap_dag used, moved from plan-selection) and expand_pane_chains_in_workload. Pane expansion now memoizes panes by (chain, index): two merges over one chain (p99 and p50 over the same hour) read one set of panes, not two.
CostModel, ResourceUnitCost and PlanSelectionSettings could only be
built in Rust, so a deployment could not pass its prices as a file
(only --measured-operation-costs was loadable). PlanSelectionSettings,
CostModel, ResourceUnitCost, ResourceSpeed, ClusterLimits and
ClusterObjective now serialize: the enums in snake_case
({"usage_over_time": {...}}), every struct refusing unknown fields, and
an omitted PlanSelectionSettings field taking its default.
stage_pipeline --plan-selection-settings <file> reads the whole settings
from one such file; it replaces --cost-model, the --cluster-* flags and
--measured-operation-costs, and combining them is refused.
…lic API A runtime turned Remote Write samples into input rows with physical_planner::promql_rows::series_row and read answer labels with decode_series_identity: the executor's row encoding. Binding rows at a pane's TimeRange also skipped the Scan, so label matchers were dropped, and an instant selector counted every sample in its lookback. RawSampleSelector::new(dag, window) prepares one raw-sample window once: metric() routes samples by metric, and rows(window_end_ms, samples) keeps the samples in (end - width, end] that the Scan's compiled predicates keep and, for an instant selector, only each series' latest one, in the TimeRange's schema. series_labels_of_output_row reads an answer row's labels; Sample is the input. promql_rows and frontier_from_timing become crate-private; the test-support feature, which the Planner's own executor and integration tests enable, keeps them reachable for those tests.
… offset Stage 3 retained a kept pane chain for its window plus the longest range span of its readers, measured as if its newest pane ended at the evaluation. A chain over an offset input (`max_over_time(m[60m] offset 10m)`) has its newest pane end 10 minutes earlier, so its panes reach back 70 minutes: Selection::pane_retention_ms reported 3,600,000 ms instead of 4,200,000, and the kept memory priced from it was one pane short. The retention now adds the offset of the TimeShifts directly below the chain (end offset + N·w + span); without an offset nothing changes, and every pinned number stays as it was.
zzylol
force-pushed
the
feat/runtime-handoff-749
branch
from
October 18, 2026 22:55
bf1bb85 to
f7f4254
Compare
A runtime had to link Planner internals to install Stage 3's plan: run the stages, expand pane chains, pick the executor's summary families, find the nodes to store (frontier_from_timing) and walk the DAG for each pane's window. plan_installable(UserInput) plans the workload as e2e_plan does and returns an InstallablePlan, the runtime's plan file: - dag: the selected physical ASAP DAG in its versioned document, with Stage 3's timings and materialization and pane chains expanded; - queries: each query's text and the root node that answers it; a standalone scalar query is refused (UnsupportedQueryRoot); - stored_outputs: each node stored across evaluations, with the run that builds it (an ingestion run, or the query run for a result kept across evaluations), a computation id shared by panes that differ only in offset (so a runtime builds each pane once), its pane (a RawSampleWindow), and how long to keep it (Stage 3's pane_retention_ms of its chain, or how far the pane reaches back); - query_time_raw_windows: the other raw-sample windows read at query time; a window that is not a fixed offset from the evaluation time (a subquery or TimeShift above the range, an @ anchor below it) is refused; - selection: the selected candidate id and its cost per second. Unrestricted supported summaries default to the executor's families. InstallablePlan::query_time_reference (validating first) gives the correctness reference for a runtime's tests. The stage export keeps the chains for costing and display. asap_planner re-exports what a runtime needs to build UserInput, so it depends on asap_planner and asap_executor alone; CI checks asap-planner's tests on their own, where the executor links without test-support, so the runtime test uses public APIs only. crates/planner/tests/runtime_install.rs runs #509 Example 4b the way a runtime does, on samples whose values name their pane, plus a label matcher, an instant selector, two queries whose panes build once, and query-time results kept across evaluations; the integration tests check the plan against frontier_from_timing, two queries on one chain, the refused shapes and scalar queries, and the panes' retention under a range evaluation and an offset.
zzylol
force-pushed
the
feat/runtime-handoff-749
branch
from
October 19, 2026 01:50
f7f4254 to
729901b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A deployment runtime (ASAPQuery-backend or any other) should call ASAPPlanner once, get back the single selected DAG, and install it. It needs three things from the plan: which DAG to install, which outputs to store, and how to feed samples in. Today the backend links Planner internals for each of these (#749):
run_stage1_to_stage3;frontier_from_timingand walks the DAG for each pane's window;promql_rows::series_row;L<n>toP<n>);What
One facade entry point.
asap_planner::plan_installable(UserInput) -> InstallablePlanplans the workload ase2e_plandoes and returns the runtime's plan file:dag: aPhysicalASAPDAGDocument, the versioned envelope checked byvalidate. It holds the selected DAG with Stage 3's timings and materialization, pane chains already expanded into the panes the executor runs.queries: each query's text androot_node_id, the DAG root that answers it. A standalone scalar query (e.g.scalar(sum(m))) has no node that answers it, so it is refused withUnsupportedQueryRoot { entry_index }.stored_outputs: each node stored across evaluations, withbuilt_at, acomputation_id, itspaneand itsretention_ms. Every output of onecomputation_idcarries the largest retention among them, since a runtime keeps their panes as one.built_atis eitherIngestionRun, orQueryRunfor a query-time result kept across evaluations: the evaluation that first needs it builds it, then the runtime stores it. The materialization is read from the node itself. Panes that differ only in offset share acomputation_id, and it is equal across plans of one wire version. Thepaneis aRawSampleWindow:TimeRangewidth, summedTimeShiftoffset,Scannode. A stored node without a pane is refused (StoredOutputWithoutPane).query_time_raw_windows: the raw-sample windows read at query time, other than stored outputs' panes. A kept query-time result's range is fed only through its build, never twice.selection:selected_candidate_idandcost_per_second, for reporting.Refused shapes. A raw-sample window whose position is not a fixed offset from the evaluation time is refused with
UnsupportedRawSampleWindow { time_range_node_id, reason }. That covers aPromqlSubquery,TimeShiftorTimeRangeabove the range, an@anchor below it, a non-TimeShiftoperator between the range and itsScan, or a negative total offset below it. A negative offset gives a window that ends after the evaluation, which no ingestion run can build in time. Such windows are no longer listed with a wrong window or silently dropped. I chose refusal over computing windows from ancestors: it is the simpler correct option, and those shapes can be supported later.Pane-chain expansion is internal, and panes are memoized by (chain, index). Two queries on one chain (p99 and p50 over the same hour) now store 6 panes, not 12. The stage export (
PlanOutput::physical_asap_dag,stage_pipeline, the viewer) keeps the chains for Stage 2/3 costing and display.Supported summaries default to the executor's. When
DeploymentCapabilities.supported_summariesisNone,plan_installableoffers only what the Planner's executor runs per instance (asap_executor::supported_summary_families(), cross-checked against the factory and the evaluations). Count-Min listsTotalonly. Its item frequency comes only from a Hydra grid, which Stage 2 does not produce yet;SupportedSummarygains a grouping dimension when it does. Raw-data and query-time-result retention stay deployment inputs.Samples in, labels out.
asap_executor::RawSampleSelector::new(&dag, &window)prepares one raw-sample window once:.metric()says which metric's samples to route to it..rows(window_end_ms, samples)keeps the samples in(window_end_ms - width_ms, window_end_ms]that the window'sScanpredicates (its label matchers) keep. For an instant selector, it keeps only each series' latest sample in the lookback. Rows come back in theTimeRange's schema.series_labels_of_output_rowreads an answer's labels, andSampleis the input.promql_rowsis apub(crate)module, andfrontier_from_timingis compiled only undercfg(any(test, feature = "test-support")). Thetest-supportfeature makes both public for the Planner's own executor and integration tests.Correctness reference.
InstallablePlan::query_time_reference()validates first, then returns the same summaries and parameters with nothing materialized. It is for a runtime's tests only.Building each pane once.
StoredOutput.computation_idexists for this: a runtime builds each distinct(computation_id, pane.end_offset_ms)once. For example,[60m]and[60m] offset 10mstore 12 panes of one computation, and 7 are built.One dependency pair.
asap_plannerre-exports what a runtime needs to buildUserInputand its deployment inputs, and to read the plan. That includesSketchAlgorithm,StatisticKind,SketchParams,SummaryOperationandHistogramKind. A runtime depends onasap_planner+asap_executoronly. A new CI step,cargo check -p asap-planner --tests --locked, enforces this: the runtime test (crates/planner/tests/runtime_install.rs) builds there with the executor linked withouttest-support.Cost-model settings load from JSON:
PlanSelectionSettingsand the types under it, plusstage_pipeline --plan-selection-settings <file>, which cannot be combined with--cost-model, the--cluster-*flags or--measured-operation-costs.Pane retention. Each stored pane carries
retention_ms: how far back from the newest evaluation the runtime keeps it. The value is Stage 3'sSelection::pane_retention_msfor its chain (end offset + N·w + range span), or how far the pane reaches back when Stage 3 records none. A separate Stage 3 commit fixes the offset part: retention and the kept-memory price used to ignore a chain's input offset, so[60m] offset 10mwas retained for 3,600,000 ms instead of 4,200,000, and its kept memory was priced one pane short. No pinned number changes for workloads without offsets.Doc fix (Runtime handoff: gaps in consuming Stage 3's selected plan outside the Planner #749 item 1).
PlanOutput::physical_asap_dagalready keeps Stage 3's timings; its doc said otherwise. The doc is corrected and the behavior pinned by a test.Names
plan_installable/InstallablePlan: besidee2e_plan/PlanOutput(every query's selected IR plus the stage record), the name says what this one returns: the plan a runtime installs.queries: each entry holds the query text and its root, so it is not calledroots.RawSampleWindow { width_ms, end_offset_ms }: a window of raw samples, as opposed to a node input.RawSampleSelectorprepares one for rows.query_time_reference(): the plan a runtime's correctness test compares against.Changes since review of 3246976
input_rows_from_samplestakes(dag, &RawSampleWindow, evaluation_time_ms, samples)and runs the plan'sScan(replaced byRawSampleSelectorin round 2); matcher e2e case addedexpand; types and integration regression testsUnsupportedRawSampleWindow(subquery,TimeShiftabove,@below); a plainoffset 5mrange is listed correctlyStoredOutput.pane: RawSampleWindow,StoredOutputWithoutPane { node_id };RawSampleInputrenamedRawSampleWindowand moved toasap-types, since the executor takes itItemFrequencydropped; the cross-check is per-instancedag: PhysicalASAPDAGDocument, checked invalidatewith_every_node_at_query_timevalidates first and returnsInstallablePlanErrorcrates/planner/tests/runtime_install.rs(executor withouttest-support), no internal importsSelectedPlanReport { selected_candidate_id, cost_per_second: Option<f64> }; removedresource_usage,accepted_accuracy_per_query,StoredOutput.materialization,RawSampleInput.scan_source(theScannode holds it); theResourceUsage/QueryLatencyserde derives they needed are revertedasap_plannerre-exports;InstallablePlanError::ExportrenamedTimingChanges since review of c021915
RawSampleSelector::rowskeeps each series' latest sample in an instant lookback;sum(latency_ms)with a@T-900=1, a@T-100=10, b@T-200=100 now gives 110 (111 before, test written first)input_rows_from_samplesis replaced byRawSampleSelector::new(&dag, &window)?with.metric()and.rows(window_end_ms, samples); theScanpredicates are compiled once per window; ingestion passes the pane end. No label-matcher accessor: matchers stay compiled predicates (exposing them as data would mean decoding the predicate IR)nodes.remove(0)→InvalidDag; without the guards it panicked (confirmed)format-and-lintrunscargo check -p asap-planner --tests --locked; importingfrontier_from_timingthere fails to compile (confirmed); body claims fixedPlanError::Installablereworded(computation_id, end_offset_ms)once; test:[60m]+[60m] offset 10m→ 12 stored outputs, 1 computation_id, 7 builds, answers equal the referenceTimeShift-above-the-range refusal on a hand-built DAG; executor unit tests of the selector (window bound edges, instant latest rule, metric routing and matchers, a tableScanrefused)SketchAlgorithm,StatisticKind,SketchParams,SummaryOperation,HistogramKindwidth_ms/end_offset_ms;query_time_reference()validatecompares without cloning;of_dagkeeps validation as adebug_assert; raw-sample reachability and "re-timed from above" are computed once per DAG in O(nodes + edges) passes (bottom-up and top-down over one postorder)Changes since review of bf1bb85
1000 × pane + base), and the e2e tests assert which pane each p99 comes from (5 for the hour, 6 for the offset hour, 0 for the 5-minute max); changingbounds_msto ignoreend_offset_msfails 3 of them (confirmed). Unit test: width 600,000, offset 300,000, T 3,600,000 → (2,700,000, 3,300,000)UnsupportedQueryRoot { entry_index };InstalledQueryRootis removed and each query carriesroot_node_idStoredOutput.built_at: StoredOutputBuild { IngestionRun, QueryRun }. A stored output's pane range is not listed inquery_time_raw_windows; dropping that filter fails the new test (confirmed). The Usage branches onbuilt_at. Test: aquery_time_results_retaineddashboard panel stores 5 query-run panes, builds each once, and equals the reference--plan-selection-settingscannot be combined with the other flagsRawSampleSelector::newcompiles the Scan's predicates straight from its payload (no DAG rebuild);restoreis private againStoredOutput.retention_ms, from Stage 3; Stage 3's retention and price include the chain's offset (own commit, test first)5a0318d1(#759); one conflict, both PRs appending astage_pipelinetestChanges since review of f7f4254
computation_idcarries the largestretention_msamong them; the offset-twin test asserts all 12 carry 4,200,000 (failed first: half carried 3,600,000)UnsupportedRawSampleWindow { reason: "a negative offset below the range" };max_over_time(latency_ms[60m] offset -10m)added to the refused-shapes test (failed first: it planned)runtime_install.rsfixed. The data plane keeps a pane store across evaluations, keyed by (computation, pane end), and evicts byretention_ms. The kept-results test evaluates again at T + 60 s and builds 1 pane, reusing 4runtime_handoffhas 8 testsexpandedandExpandedWithRetention.retention_ms_by_nodeHow
plan_installable:supported_summariesfrom the executor when it isNone.e2e_plan.apply_materialization_timings_to_roots).expand_pane_chains_in_workload, one memo for nodes and panes, and export.queries,stored_outputsandquery_time_raw_windowsfrom the DAG and validate.TimeRangethat reaches aScanwithout anotherTimeRangeis checked. Only offsetTimeShifts may sit between it and theScan, and noTimeShift/TimeRange/PromqlSubquerymay sit above it. Query-time ones are listed; an ingestion-time one is fed through its stored output's pane, found by following single inputs down to theTimeRange.computation_id: SHA-256 over each node's payload (child ids zeroed) and output schema, then its inputs' hashes; an offsetTimeShifttakes its input's hash. This is the backend'sDefinitionIdscheme.RawSampleSelector::newchecks the window is a time-seriesScanunder aTimeRangewith the same columns, and compiles theScan's predicates over its schema (CompiledExpression).rowsthen:TimeRangeKind::Instant, keeps each label set's latest sample.IndexedDagderives two sets in one pass each, from one children-first postorder: which nodes read raw samples (bottom-up), and which have aTimeShift/TimeRange/PromqlSubqueryabove them (top-down).#[cfg(feature = "test-support")] pub mod promql_rows/pub(crate) mod, andfrontier_from_timingundercfg(any(test, feature = "test-support")). This avoids rewriting the 11 Planner test files that use them.Key code interfaces added / modified
crates/planner/src/installable.rs(new; re-exported fromasap_planner)crates/planner/src/lib.rscrates/types/src/ir/physical_export.rscrates/types/src/ir/properties/timing.rs,crates/types/src/ir/operator/pane_chain.rscrates/types/src/ir/schema/family_capability.rs(also re-exported at its old path fromasap_logical_asap_optimization)crates/executor/src/samples.rs(new; re-exported fromasap_executor)crates/executor/src/supported_summaries.rs(new; re-exported fromasap_executor)crates/executor/src/physical_planner/mod.rs,candidates.rs,Cargo.toml.github/workflows/rust.ymlcrates/plan-selection/src/lib.rscrates/plan-selection/src/lib.rs(Stage 3 retention fix)crates/plan-selection/src/pass/mod.rs:PlanOutput::physical_asap_dagis unchanged; only its doc changes (each node keeps the timing its pass gave it).crates/devtools/src/bin/stage_pipeline.rsUsage
This is the code of
crates/planner/tests/runtime_install.rs, run at this head. It imports onlyasap_plannerandasap_executor. CI checks it withcargo check -p asap-planner --tests --locked, where the executor links withouttest-support, so the compiler enforces that no internal is used.The workload is #509 Example 4b (
quantile_over_time(0.99, latency_ms[60m])every 10 minutes, rank error 0.01 at δ 0.01, 1,000 ingesting series sampled every second), plus itsoffset 10mtwin. The samples are seriesaandb, every 10 s over the 70 minutes beforeT= 3,600,000 ms, plus one sample of a metric the plan does not read. Each value is1000 × pane + base + 0..=100: pane 0 is the newest 10 minutes, and base is 100 fora, 300 forb. A wrong window therefore gives a visibly wrong answer.Control plane: the workload and the deployment inputs, one
plan_installablecall,plan.json.Data plane, at one evaluation: from
plan.json, the samples, and the panes earlier evaluations left in the store. Each distinct(computation_id, pane end)the store lacks is built once, in the run itsbuilt_atnames. Then the queries run, panes past theirretention_msare dropped, and the query-time reference runs.The runtime's compile-and-run with the executor:
What the runtime gets back:
offset 10mtwin there are 12 stored outputs of one computation, each kept 4,200,000 ms, and 7 builds; the twin's p99 comes from pane 6.Tand only 1 atT+ 60 s, reusing 4 from the store.Before this PR
run_stage1_to_stage3and expanded pane chains itself; the executor rejects an unexpanded chain.supported_summaries: Noneadmitted families the executor cannot run.frontier_from_timing, and each pane's window from walking the DAG (backendsds.rspane_of/identity).promql_rows::series_row, and answer labels read withdecode_series_identity.After this PR
A runtime builds the workload and its deployment inputs, calls
plan_installable, writes theInstallablePlanas its plan file, and runs it withRawSampleSelectorand the executor'scompile/instantiate/execute, with no other Planner call:stored_outputsreplacesfrontier_from_timingplus the SDS DAG walk.RawSampleSelectorreplacespromql_rowsand applies the plan's matchers and the instant-selector rule.InstallablePlan::query_time_referencereplaces the id-matching reference.Verification
Only the tests covering the change, plus fmt, the new CI check and clippy on the touched crates:
cargo nextest run -p asap-planner: 29 passed.runtime_install(6 tests):[60m]+[60m] offset 10m: 12 stored outputs, 1computation_id, 7 builds, p99s from panes 5 and 6.QueryRunpanes, none also listed as query-time windows. 5 builds atT, then 1 build atT+ 60 s (4 reused); both evaluations equal the reference.a_time_shift_above_the_range_is_refused, and the default to the executor's families.bounds_mschanged to ignoreend_offset_ms, 3runtime_installtests fail; with the stored-pane filter removed, the kept-results test fails.cargo check -p asap-planner --tests --locked(the CI step): passes.cargo nextest run -p asap-integration-tests --test runtime_handoff: 8 passed.UnsupportedQueryRoot).offset -10m.--test range_evaluation: passed, including the new Stage 3 test. It failed first: the offset chain reported 3,600,000 ms. It now reports 4,200,000 ms, and the kept memory is priced higher.asap-plan-selection(171),asap-integration-tests(273) andasap-devtools(42) suites passed with no number changed.--test tumbling_pane_merge --test materialization_e2e --test frequency_execution: passed.cargo nextest run -p asap-types --test physical_export --test pane_chain: 19 passed, including thebounds_msoffset test.cargo nextest run -p asap-executor -E 'test(samples::) | test(supported_summary) | test(frontier)': 12 passed.cargo fmt --all -- --check: clean.cargo clippyon the 7 touched crates with--all-targets --all-features --locked -- -D warnings: clean.cargo clippy -p asap-planner --all-targets --locked -- -D warnings(notest-support): clean.cargo clippy -p asap-executor --lib --locked -- -D warnings: clean.Limitations and follow-ups
UnsupportedRawSampleWindow): windows under a subquery, under aTimeShiftabove the range, or with an@anchor below it. Supporting them means computing the window from the ancestors and carrying an absoluteat.RawSampleSelectorexposes the metric, but not the label matchers as data. Matchers run as theScan's compiled predicates.scalar(...), a constant) are refused (UnsupportedQueryRoot). Answering them needs a public executor entry for scalar roots.RawSampleSelector::rowsencodes every in-window sample of its metric before applying theScan's label matchers. With K windows over one metric that is K encodings per sample in their windows (a pane's samples fall in one window per computation). Matching label-only matchers onSample.labelsfirst would avoid that. It needs a decoder for the predicate IR's column comparisons, so it is not done here.SupportedSummaryhas no grouping dimension, so Count-Min's Hydra-only item frequency is not listed. It is added when Stage 2 produces Hydra.computation_idhashes the serialized DAG payload, so it is stable within onePHYSICAL_ASAP_DAG_WIRE_VERSION.validaterefuses other versions.promql_rowsstays reachable throughtest-supportfor the Planner's own tests. Moving those tests onto the public sample API would let it become fully private.plan_installablebelong to ASAPQuery-backend.Closes #749