Skip to content

fix(planning): derive the fallback window implementations from the query - #696

Closed
zzylol wants to merge 2 commits into
mainfrom
fix/window-candidate-shape
Closed

fix(planning): derive the fallback window implementations from the query#696
zzylol wants to merge 2 commits into
mainfrom
fix/window-candidate-shape

Conversation

@zzylol

@zzylol zzylol commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Why

A workload of two 5m-lookback quantiles evaluated every 30s compiled to a plan
whose state advances once every five minutes:

"window_size": 300, "slide_interval": 300, "window_type": "tumbling",
"window_layout": { "kind": "pane", "pane_secs": 300 },
"num_aggregates_to_retain": 2

Nothing rejected it, because nothing had planned it. The query's evaluation
cadence was parsed, used for lifecycle costing, and then dropped before the
window shape was chosen.

Worse, and found while reviewing the first fix: that fallback was a single
candidate built from time_selection.lookback. A query carries one range
selector per operand, and under hybrid_execution a selected state whose window
has no candidate is filtered out of selection outright. So
sum(sum_over_time(a[1m])) / sum(sum_over_time(b[5m])) under a 1m lookback kept
a and silently dropped b to exact execution — no error, no report.

What

The fallback window implementations — synthesized when a snapshot prices none
for a query — are now derived from the query itself: one per distinct
range-selector window
, each shaped by the query's evaluation cadence.
Snapshots that supply window_candidates are unaffected.

Plan-compile time only. No change to the wire contract, to serving, or to any
snapshot that carries its own priced candidates.

How

compiler.rs's unwrap_or_else produced one lookback-wide tumbling window with
a lookback-wide pane, ignoring both the range selectors in the query and the
evaluation_interval_ms parsed ~60 lines above. derived_window_candidates
replaces it.

range_selector_windows_secs walks the canonical pre-ASAP expression for
TimeRange nodes, so each operand gets a candidate for its own range.
window_implementation_id reaches lifecycle estimates and cost manifests, so a
multi-window query suffixes it per window while a single-window query keeps the
snapshot's identity byte-for-byte.

Within one window the shape follows the cadence:

  • cadence shorter than the window and dividing it → slide by one evaluation
    interval, store Pane { pane_secs: slide_secs }, framework Sliding;
  • otherwise → the previous tumbling shape, unchanged.

The guards are the validator's own rules, so a bad derivation would be a compile
error rather than a silent plan: window_secs must equal PlanningQuery::window_secs;
WindowMaterializationLayout::validate requires the pane to divide both window
and slide (45s into 300s has no such pane); the framework/layout table admits
Tumbling + Pane and Sliding + Pane. pane_secs == slide_secs is the
coarsest legal pane for a given cadence — the fewest query-time merges.

This derives a shape, never a price. ImplementationCostEvidence::weighted_cost
is explicit that the evidence producer prices update CPU, query-time merges,
retained memory, storage, scans and network. So this does not synthesize a
second candidate to rank against the first: a lone candidate is chosen by
complete_summary_candidate_estimate's min_by over a one-element list, where
the cost value cannot change the outcome. Ranking Pane against FullWindow
still requires a snapshot supplying both with their own evidence — see
Limitations.

Before this PR

Every query without supplied candidates got exactly one candidate: a
lookback-wide tumbling window, whatever its cadence and whatever ranges it
actually selects. num_aggregates_to_retain followed — (lookback + staleness) / pane + 1 with pane == lookback is always 2. Any operand whose own range
differed from the declared lookback lost its summary without a word.

After this PR

The same workload plans as window 300 / slide 30 / sliding / Pane{30},
retaining 11 states. Each sample updates exactly one 30s pane
(worker.rs's non-FullWindow branch), and a readout composes ten of them.

Each operand of a multi-range query keeps its own window and readout:
sum(sum_over_time(a[1m])) / sum(sum_over_time(b[5m])) now installs both, where
before b fell back to exact execution.

A single-range query whose cadence equals its lookback, or does not divide it,
is planned exactly as before.

Evidence

Execution example — the reported workload, from
five_minute_lookback_evaluated_every_thirty_seconds_slides_by_thirty:

query    quantile_over_time(0.5, data[5m])
demand   fixed_interval_at { interval: 30000 }
lookback 300000

before   window_size=300 slide_interval=300 tumbling  Pane{300}  retain=2
after    window_size=300 slide_interval=30  sliding   Pane{30}   retain=11

Execution example — derivation across cadences, from
derived_window_candidate_follows_the_evaluation_cadence and
derived_window_candidate_stays_tumbling_without_a_dividing_cadence:

lookback cadence framework slide layout
300s 30s Sliding 30 Pane{30}
300s 300s Tumbling 300 Pane{300}
300s 450s Tumbling 300 Pane{300}
300s 45s (does not divide) Tumbling 300 Pane{300}
300s 0 Tumbling 300 Pane{300}

Performance measurement: not applicable — no runtime path changes. Choosing
between layouts on measured cost is explicitly out of scope here (Limitations).

Screenshot: not applicable.

Verification

  • Unit tests:
    • derived_window_candidate_follows_the_evaluation_cadence — a 30s cadence
      over a 5m lookback yields sliding / 30s slide / 30s panes.
    • derived_window_candidate_stays_tumbling_without_a_dividing_cadence — a
      cadence at, above, or not dividing the window keeps the previous shape.
    • derived_window_candidate_shapes_are_accepted_by_validation — every shape
      this function can emit passes validate_window_implementations, so a bad
      derivation cannot reach a plan.
    • supplied_window_candidates_are_not_replaced_by_the_derivation — a snapshot
      carrying its own priced candidates keeps them verbatim.
    • retained_state_count_follows_the_derived_pane_width — end to end, the
      retained count is derived from the pane width (six 10s panes for a 1m
      lookback, plus the one filling → 7).
    • five_minute_lookback_evaluated_every_thirty_seconds_slides_by_thirty — the
      reported case, end to end.
    • composable_binary_binds_independent_source_windows — reused as the
      multi-range regression: it no longer hand-supplies the 5m candidate its
      second operand needed, and both operands keep their own range.
    • Full suite: cargo test -p control_plane — 807 passed, 0 failed (768 lib =
      762 pre-existing + 6 new, plus 31 api + 8 integration).
  • End-to-end tests: not run. cargo test -p data_plane --lib does not compile
    in this checkout — asap_sketchlib is missing accepts_standard_updates
    (univmon_accumulator.rs:48) and quantile_interpolated
    (summary_executor.rs:1053). Pre-existing and unrelated (this PR touches one
    control-plane file), but it means no data-plane evidence is offered here.
  • Other checks: cargo fmt -p control_plane -- --check clean; cargo clippy -p control_plane --all-targets reports no new warning in the changed file.
  • Three existing assertions changed, each pinning the previous artifact rather
    than intended behavior. Called out individually because two of them turn an
    exact path into a summary path:
    • composable_binary_binds_independent_source_windows — hand-supplied a 5m
      candidate so its second operand would survive. The workaround is deleted;
      both operands now keep their own range, with 10s panes for the 10s cadence.
    • composable_binary_retains_summary_sibling_of_prometheus_filtered_subtree
      — asserted a filtered denominator stays a typed residual. Nothing about the
      filter forced that: the missing 5m candidate did. Verified directly that the
      installed state carries {job!="x"} and the binding's
      population_filter_canonical matches, so the query reads a summary over its
      own filtered population rather than an unfiltered sibling. Renamed to
      composable_binary_summarizes_each_prometheus_filtered_operand and now pins
      filter and range together. The residual path itself loses this test's
      coverage
      — it still exists, but no test in this file exercises it now.
    • counter_materialization_manifest_prices_owned_state_and_distinct_native_alternative
      — required an exact_backend component. Its 5m operand landed there for the
      same reason and is now summarized.

Architectural decisions

Derive from the query, not from the declared lookback. time_selection.lookback
is the workload's declared range; the range selectors are what each operand
actually computes over, and each becomes its own materialization. Rejected
alternative: keep one candidate and widen it to the largest range. That would
give short-range operands a state far wider than they read.

Derive the shape, not the price. Rejected alternative: emit several
candidates (Pane{30}, Pane{10}, FullWindow) with synthesized costs so the
existing ranking has something to compare. The ranking machinery is genuinely
ready — RealizationProvider::windowswith_window_implementation_costs
complete_summary_candidate_estimate is wired and live — but feeding it costs
this compiler invented would be fabricating measurement, against the explicit
contract on weighted_cost. A lone candidate needs no price to be selected,
which is why fixing the shape needs no new evidence.

Pane, not FullWindow, for the derived sliding shape. Both are legal Sliding
layouts. Preferring FullWindow is a write-amplification-versus-read-amplification
tradeoff (each sample updating window/slide overlapping states versus one
pane), i.e. exactly the comparison that needs two priced quotes. With one
candidate available, the pane form is chosen because it does not multiply
per-sample update work.

pane_secs == slide_secs. The validator requires the pane to divide the
slide, so any legal pane is at most one slide wide; the widest one is the
cheapest to read. Note realization.rs's module doc forbids a realization
provider
from inferring pane width from slide — this derivation lives at
snapshot ingestion instead, which is where the cadence is known.

Limitations and follow-up

  • Layouts are still not ranked against each other. A deployment that wants
    FullWindow compared with Pane must supply both in window_candidates,
    each with its own ImplementationCostEvidence. The control plane could become
    that producer by composing the snapshot's supplied lifecycle unit costs
    (build, maintenance_per_update, read, retention_per_second) with each
    layout's structural counts — update fanout, merges per read,
    retained_state_count — which would be a cost model rather than invented
    magnitudes. It is deliberately not in this PR: it changes which layout every
    sliding query selects, driven by a formula a reviewer has to check line by
    line, and folding it in behind a shape fix would hide exactly the part that
    needs reviewing.
  • Derived materializations still require slide == window.
    maintenance_runtime.rs:1108 and :1269 reject a sliding cohort for
    materializations with a derived_input. Live-ingest materializations (the
    reported case) are unaffected, and the gate fails closed with a clear message
    rather than computing something wrong. Lifting it is not a one-line change and
    is not attempted here: :1163 filters source windows with
    (window.0 - pane_origin_ms).rem_euclid(width), where width is the full
    window, so a sliding cohort whose sources arrive every slide would have nine
    of every ten windows silently skipped. That filter, the two gates, and the
    readout semantics for overlapping states need one change with its own
    validation — and cargo test -p data_plane does not compile in this checkout,
    so none of it could be verified here.
  • window_implementation_id is not renamed by the derivation. It is the
    snapshot's identity for its priced implementation, so a demo value of
    backend-tumbling-v1 now labels a sliding candidate. Confusing to read, but
    renaming deployment-supplied identity here would be worse.
  • Hierarchical rollup is never derived — it needs the Extension( "backend.exact-hierarchical-rollup.v1") framework and its own evidence.

Human review — do not complete with an agent

  • The MVP boundary is correct.
  • New conceptual layers or public interfaces are necessary.
  • The before/after description matches the intended product behavior.
  • Human reviewer:
  • Decision and rationale:

🤖 Generated with Claude Code

zzylol and others added 2 commits September 12, 2026 10:00
…adence

A snapshot that prices no window implementation for a query got one
synthesized here, hardcoded to a lookback-wide tumbling window with a
lookback-wide pane. The query's own evaluation cadence was parsed a few
lines above, used for lifecycle costing, and then dropped.

The result is a plan whose answer only changes once per window. A
workload of two 5m-lookback quantiles evaluated every 30s planned as
window 300 / slide 300 / tumbling / Pane{300}: the state advances every
five minutes while the queries run every thirty seconds. The retained
count followed it down to 2, which is arithmetically right for a
300s pane and useless for a 30s cadence.

Derive the shape instead. When the cadence is shorter than the window
and divides it, slide by one evaluation interval and store panes of that
width; otherwise keep the previous tumbling shape. The same workload now
plans as window 300 / slide 30 / sliding / Pane{30}, retaining 11 states
-- ten panes covering the lookback plus the one still filling.

This derives a shape, never a price. `ImplementationCostEvidence` is
measured evidence: its `weighted_cost` doc puts pricing update CPU,
query-time merges, retained memory, storage, scans and network on the
evidence producer. So this does not synthesize a second candidate to
rank against the first -- a lone candidate is chosen by a `min_by` over
one element, where the cost cannot change the outcome. Ranking Pane
against FullWindow still requires a snapshot that supplies both with
their own priced evidence, and `window_candidates` already carries them
untouched when it does.

Two existing assertions encoded the old default's pane width. The
snapshot they load evaluates every 10s over a 1m lookback, so its stored
pane is now 10s; both readouts keep their 1m `readout_lookback_ms`, and
every other property those tests assert is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The derived fallback built a single candidate from
`time_selection.lookback`. A query carries one range selector per
operand, and they need not agree with the declared lookback or with each
other.

The consequence is silent. Under `hybrid_execution`, a selected state
whose window has no candidate is filtered out of selection entirely, and
the surviving candidates are then narrowed to the selected window before
validation. So `sum(sum_over_time(a[1m])) / sum(sum_over_time(b[5m]))`
under a 1m lookback kept `a` and dropped `b` to exact execution, with
nothing reported and no error raised.

Derive one candidate per distinct range-selector window instead, each
shaped by the evaluation cadence as before. `window_implementation_id`
reaches lifecycle estimates and cost manifests, so a query with several
windows suffixes it per window; a single-window query keeps the
snapshot's identity untouched.

Three assertions changed, all of them pinning the previous artifact
rather than intended behavior:

- `composable_binary_binds_independent_source_windows` hand-supplied a
  5m candidate so `b` would survive. The derivation now covers it, so
  the workaround is gone and both operands keep their own range.
- `composable_binary_retains_summary_sibling_of_prometheus_filtered_subtree`
  asserted a filtered denominator stays a typed residual. Nothing about
  the filter forced that -- the missing 5m candidate did. It now gets a
  summary over its own filtered population, which the renamed
  `composable_binary_summarizes_each_prometheus_filtered_operand` pins,
  filter and range together.
- `counter_materialization_manifest_prices_owned_state_and_distinct_native_alternative`
  required an `exact_backend` component. Its 5m operand landed there for
  the same reason and is now summarized.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zzylol zzylol changed the title fix(planning): derive the fallback window shape from the evaluation cadence fix(planning): derive the fallback window implementations from the query Sep 12, 2026
@zzylol zzylol closed this Sep 12, 2026
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