Skip to content

refactor(planner)!: summary identity and dedup move to the runtime's SDS - #760

Merged
zzylol merged 1 commit into
mainfrom
refactor/sds-out-of-planner
Oct 19, 2026
Merged

zzylol merged 1 commit into
mainfrom
refactor/sds-out-of-planner

Conversation

@zzylol

@zzylol zzylol commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Why

User decision (2026-10-19): "make sure SDS is in runtime asapquery-backend, not asapplanner."

The Planner says what the plan needs: which nodes are stored, when each is built, its pane window, and how long that node's output must be kept. The runtime's stored-summary system (SDS) owns how: summary identity and definition ids, deduplication of identical stored outputs across queries, store layout, and eviction. The plan_installable handoff added SDS concerns to crates/planner/src/installable.rs. This PR moves them out.

Summary identity and dedup move to the runtime's SDS. ASAPQuery-backend has this on its SDS feature branches (data_plane/src/sds.rs, not yet on main). It hashes the sub-DAG and treats offset-only TimeShifts as transparent. When the backend re-pins to this Planner version, it must keep that hashing and use it. It must also keep each definition for the max retention_ms of the outputs that share it.

What

  • StoredOutput.computation_id is removed. That was the hex SHA-256 of what an output computes, and it copied the backend's DefinitionId scheme. Its hashing (computation_hash) and the planner's sha2 dependency are removed too.
  • StoredOutput.retention_ms is now that node's own requirement. The Planner no longer gives every output of one computation the largest retention among them. Its doc says that a runtime which stores identical outputs once must keep them for the largest requirement among them, and that this is the runtime's choice.
  • Storage wording ("the store lacks it", "a stored pane") is gone from the StoredOutputBuild::QueryRun and pane_of docs.

Wire format change: plan.json no longer has stored_outputs[].computation_id. StoredOutput is deny_unknown_fields, so an older plan file that still has the field is refused.

How

runtime_inputs_of builds each StoredOutput from its node only: node_id, built_at, pane, and retention_ms. The retention is Stage 3's pane_retention_ms for the node's chain, or else the pane's reach. The hashing pass and the max-per-computation pass are deleted.

The stand-in runtime test (crates/planner/tests/runtime_install.rs) now uses its own cache key, pane_cache_key. The key is built from the stored node's and its Scan's operators and output schemas with node ids removed, plus the pane width and the pane end. Offsets are left out, so panes of one chain and of an offset twin that cover the same window share an entry. The key is valid only for the shapes in these tests, where the stored node reads its pane's TimeRange directly, and the test asserts that. A cached pane keeps the largest retention_ms among the outputs it serves. That is the runtime-side rule this PR hands to the SDS.

Other SDS concepts checked in the Planner tree

I checked crates/planner, executor, types, plan-selection, physical-asap-optimization and logical-asap-optimization for stored-summary definitions, identities, store layout, eviction and dedup. Nothing else needs to move:

  • The executor states that it has none: physical_planner/compiled.rs says "without storage identity", candidates.rs says "Stored outputs have no storage identity", and types/src/physical/summary_window.rs says it does not "identify a runtime library, process, placement, shard layout, storage".
  • precompute.rs's "identities which are implicit in a stored summary instance" refers to population and pane columns in the executor's rows, not to a store.
  • The storage_io cost model in plan-selection prices storage requests. It is a cost fact, not SDS.
  • Stage 3's Selection::pane_retention_ms (one retention per chain), materialization, timing and pane windows are plan facts, and they stay.
  • "dedup" hits are the Dedup relational operator, CSE, or label-matcher dedup.

Key code interfaces added / modified

crates/planner/src/installable.rs

/// One node of the installable DAG whose output the runtime stores, and the
/// pane of raw samples it is built from. The type stays.
pub struct StoredOutput {
    /// The node whose output is stored.
    pub node_id: PhysicalASAPNodeId, // unchanged
    /// Which run builds it: an ingestion run over its pane's window, or the
    /// query run of the evaluation that first reads it.
    pub built_at: StoredOutputBuild, // unchanged
    /// Hex SHA-256 of what the output computes (summary identity).
    pub computation_id: String, // removed: summary identity is the runtime SDS's
    /// The pane of raw samples it is built from.
    pub pane: RawSampleWindow, // unchanged
    /// How far back from the newest evaluation time this node's output must
    /// stay available: Stage 3's retention of its chain, or else the pane's
    /// reach. This node's own requirement; a runtime that stores identical
    /// outputs once keeps them for the largest among them.
    pub retention_ms: u64, // was: the largest retention among outputs sharing a computation_id
}

/// SHA-256 over a node's payload and output schema, offset TimeShifts transparent.
fn computation_hash(node, dag, done) -> [u8; 32]; // removed (private)

crates/planner/Cargo.toml: dependency sha2 removed.

Usage

From an_offset_chain_keeps_its_panes_for_its_window_plus_the_offset (crates/integration-tests/tests/runtime_handoff.rs), run at this head:

// What the user writes: two repeating PromQL queries, the p99 of the last
// hour and of the hour that ends 10 minutes ago, with raw data not kept
// (`without_raw_data()`), so Stage 3 materializes 10-minute panes at ingestion.
let workload = repeating(&[
    "quantile_over_time(0.99, latency_ms[60m])",
    "quantile_over_time(0.99, latency_ms[60m] offset 10m)",
]);
// plan_installable: frontends, then Stages 1-3, then the selected DAG with each
// pane chain expanded. runtime_inputs_of lists one StoredOutput per pane node,
// each with its own chain's Stage 3 retention.
let plan: InstallablePlan = installable(&workload).await;
// Six panes per query, 12 stored outputs. There is no summary identity in the
// plan; the runtime's SDS decides that five of them are identical.
assert_eq!(plan.stored_outputs.len(), 12);
// Retention per query: the unshifted chain needs 60 min, the offset chain
// 60 + 10 min. The SDS keeps a shared definition for the max (4,200,000 ms).
assert_eq!(retention_ms_under(plan.queries[0].root_node_id), [3_600_000; 6]);
assert_eq!(retention_ms_under(plan.queries[1].root_node_id), [4_200_000; 6]);

Before this PR

The Planner hashed every stored output into a computation_id that copied the backend's DefinitionId. It also overwrote each output's retention_ms with the largest one among outputs sharing that id, which treated those outputs as a single pane store. In the twin above, all 12 outputs had retention_ms = 4_200_000. Summary identity and the retention merge, both runtime storage decisions, were made in the Planner.

After this PR

Each StoredOutput describes only its own node: what is stored, when it is built, from which pane, and for how long. In the twin, the unshifted query's six outputs have 3,600,000 ms and the offset query's six have 4,200,000 ms. Identity, dedup across queries, store layout and eviction belong to the runtime's SDS (data_plane/src/sds.rs in ASAPQuery-backend).

Follow-up (backend re-pin): keep sds.rs's sub-DAG hashing with offset-only TimeShifts transparent, use it to dedupe stored outputs, keep each definition for the max retention_ms of its outputs, and stop reading computation_id.

Verification

  • cargo nextest run -p asap-planner --test runtime_install --lib: 8 passed. The stand-in runtime runs the following on its own cache key, and every run's answers equal the query-time reference:
    • an_hour_and_its_offset_twin_both_equal_the_reference (formerly panes_of_one_computation_build_once_across_queries): 12 stored outputs, 7 panes built, both answers equal the reference;
    • query_time_results_kept_across_evaluations_are_built_in_query_runs: 5 panes, then 1;
    • the Example 4b and label-matcher runs.
  • cargo nextest run -p asap-integration-tests --test runtime_handoff: 8 passed.
    • stored_panes_carry_stage3_pane_retention keeps 7,200,000 ms for the range-evaluation case.
    • an_offset_chain_keeps_its_panes_for_its_window_plus_the_offset now checks each query's own outputs (3,600,000 / 4,200,000 ms) instead of a shared computation_id and a shared max.
  • cargo fmt --all -- --check, cargo check -p asap-planner --tests --locked, and cargo clippy -p asap-planner -p asap-integration-tests --all-targets --all-features --locked -- -D warnings: clean.
  • Screenshots and measurements: not applicable.

The Planner says what the plan needs: which nodes are stored, when each is
built, its pane, and how long that node's output must be kept. Remove
StoredOutput.computation_id (the SHA-256 that mirrored the runtime's
definition id) and the step that gave every output of one computation the
largest retention; each StoredOutput.retention_ms is now its own node's
requirement. Drop the sha2 dependency.

The stand-in runtime test dedupes panes with its own cache key and keeps a
shared pane for the largest retention among the outputs it serves.
@zzylol zzylol added binding Logical-to-physical binding and implementation selection enhancement New feature or request labels Oct 19, 2026
@zzylol
zzylol merged commit f0dc182 into main Oct 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

binding Logical-to-physical binding and implementation selection enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant