Repository navigation
Sliding window execution: support multi-window merge/subtract in query engine #554
Description
Problem
Sliding windows in the query engine today are not "tumbling with
configurable overlap" — they're a fundamentally narrower serving model. Per
asap_types::capability_matching::window_compatible
(capability_matching.rs:63-77):
match config.window_type {
WindowType::Sliding => data_range_ms == window_ms,
WindowType::Tumbling => data_range_ms.is_multiple_of(window_ms),
}A sliding-window aggregation can only serve a query whose requested time
range matches the window's width exactly — no merge, no subtract, across
multiple stored overlapping windows. This is enforced live, on every query,
via find_compatible_aggregation() (called from both
simple_engine/promql.rs:1062 and simple_engine/sql.rs:503). Tumbling
windows have had multi-bucket merge support all along
(simple_engine/mod.rs's do_merge path); sliding never got the equivalent.
Concretely: rate(http_requests_total[5m]) refreshed every 60s has
data_range_ms = 300,000 but window_size_ms = 60,000 (derived from
t_repeat_ms, unrelated to the query's own range). Under sliding this is
rejected outright — not wrong data, just "no compatible aggregation," always.
The only naturally-compatible case today is a spatial-only query with no
range literal at all, which is also the least interesting case to slide.
This blocks #555 (a manual config override to choose tumbling vs.
sliding per experiment) from being safely shippable — enabling it today
would produce plans that fail at query time for almost any real workload
query.
Relationship to #405
#405 ("Optimization-based sketch/streaming config selection") already
documents Sliding: W < range_a → Infeasible as a deliberate v1 modeling
assumption in its own feasibility table and cost formulas — not an
unimplemented case. This issue is effectively a proposal to extend that
formulation (and the corresponding query-engine implementation) to support
W < range_a for sliding windows via merge/subtract, the same way tumbling
already works. #405 remains the design/tracking issue for automatic
optimizer-driven selection generally; this issue is scoped specifically to
making sliding-window execution correct, independent of who chooses to use
one (the optimizer, or the manual override in #555).
What's already correct (not part of this issue)
The ingestion side already works and is tested:
precompute_engine/window_manager.rs's WindowManager correctly produces a
dense, Unix-epoch-aligned grid of overlapping window buckets, one every
slide_interval_ms, forever (closed_windows(), panes_for_window()).
The gap is entirely on the query-serving side.
Scope
Split into two sub-issues (linked below):
- Generating different sliding window candidates.
- Supporting sliding window candidates in the query engine (execution).
Also in scope, surfaced during design but not yet resolved:
planner/cleanup.rs'sWindowType::Slidingbranch (cleanup.rs:31-38)
hardcodes a single-window read assumption (read_count_threshold = 1) and
needs to become window-count-aware once multi-window sliding queries
exist. It also skips theCleanupPolicy::NoCleanupcheck the Tumbling
branch has — worth fixing alongside.- An unverified epoch/phase-alignment dependency for exact-match lookups:
simple_engine/mod.rs:419-423looks up a bucket at
[end_timestamp - window_size_ms, end_timestamp]exactly, but
align_end_timestamp_promql()(promql.rs:44-56) only aligns
end_timestampto the scrape interval, not to the aggregation's
slide_interval_ms. Needs resolving as part of this work (either
per-aggregation alignment, or changing exact-match to "nearest available
window").
Definition of done
- Sliding-window queries where
data_range_msis a multiple of
window_size_ms(mirroring tumbling's existing rule) return correct data,
via merge (mergeable sketch types) or subtract (subtractable types) across
multiple stored buckets. - End-to-end correctness test: plan → run → query a sliding-window
aggregation → assert correct results. (This is the test Manual windowing override (tumbling/sliding) in config.yaml #555
explicitly defers to this issue.)
Related
Full technical trace (code locations, worked example, alignment analysis)
attached as a comment on this issue.
Activity
milindsrivastava1997 commentedon Aug 20, 2026
Blocker: sliding windows need multi-window merge before the manual override is usable
Written up from a
/grillingsession scoping "add a manual tumbling/slidingoverride to
config.yaml" (see [[sliding-window-manual-override-plan]] oncethat's written up). This doc captures what got discovered about the query
engine's current sliding-window support — the reason the override work is
blocked — so it can seed its own issue/design session. Not a design for the
fix; just the findings.
The headline finding
Sliding windows, as implemented today, are not "tumbling with configurable
overlap." They're a fundamentally narrower serving model: exactly one
precomputed window per query, and the query's own requested time range must
match that window's width exactly. There is no merge/subtract path for
sliding windows at query-serving time, even though tumbling has had one all
along. This means a global "use sliding windows" toggle would silently break
almost every genuinely temporal query (anything with a PromQL range literal
or SQL WHERE-clause duration that doesn't happen to equal its own refresh
interval) — not with wrong data, but with an outright "no compatible
aggregation" failure, deterministically, every time.
Where this is enforced
asap-common/dependencies/rs/asap_types/src/capability_matching.rs:63-77,window_compatible():The doc comment says it outright: "a sliding window precomputes one fixed
range per timestamp; overlapping windows cannot be merged."
This function is called from
find_compatible_aggregation()(
capability_matching.rs:154), which is the hot-path query-servinglookup — called from both
simple_engine/promql.rs:1062andsimple_engine/sql.rs:503on every query. So this isn't a planning-timeguard that could be relaxed later without touching serving code — it's live
today, and it's the actual reason a sliding-window query with a mismatched
range would fail.
Concrete example that fails today
Query:
rate(http_requests_total[5m]), in a query group withrepetition_delay_ms: 60000.data_range_ms= 300,000ms — comes straight from the[5m]literal in thequery text (
query_requirements.rs:68-78; SQL's equivalent isTimeInfo::get_duration()viasql.rs:255,compute_sql_window()).window_size_ms= 60,000ms — comes fromt_repeat_msviaget_effective_repeat()(planner/window.rs:69), not fromdata_range_ms.data_range_msonly participates as a floor check(
data_range_ms >= t_repeat_ms,window.rs:64-68).Tumbling handles this fine: at query time it's a range query over
[start, end), merging however many 60,000ms buckets fall inside(
simple_engine/mod.rs:416,426,do_merge);300000 % 60000 == 0passeswindow_compatible.Sliding cannot:
300000 != 60000→window_compatiblereturnsfalse→find_compatible_aggregationfinds no candidate for this statistic → thequery fails to be served, always, regardless of workload timing.
The only query shape that's naturally compatible with sliding today is a
spatial-only query with no range literal at all (e.g.
sum(x)), becausedata_range_msdefaults to one scrape interval andset_window_parameters'sspatial relaxation branch (
window.rs:61-63) forceswindow_size_mstomatch it. That's also the least interesting case to slide — the window is
already the system's smallest granularity, so there's nothing meaningful left
to overlap.
What already works (ingestion side) vs. what doesn't (query side)
The ingestion side is actually already correct and tested:
precompute_engine/window_manager.rs'sWindowManagerproduces a dense,Unix-epoch-aligned grid of completed sliding-window buckets — one every
slide_interval_ms, forever (closed_windows(),panes_for_window(),covered by existing unit tests at the bottom of that file). The worker really
does emit overlapping windows on schedule.
The gap is entirely on the query-serving side: nothing merges or
subtracts across multiple stored sliding-window buckets to answer a query
whose range spans more than one window.
simple_engine/mod.rs'screate_store_query_plan(:413-427) andexecute_and_merge_store_queries(
:544-576) both branch unconditionally onwindow_type == Sliding→single exact-match lookup, explicitly skipping the merge path that Tumbling
uses (
"Sliding window mode: Skipping merge (expecting 1 precompute per key)",mod.rs:553).optimizer/candidate_gen.rsalready has a conceptual design for this inthe cost-model/candidate-generation space —
window_candidates()(
:104-151) enumerates sliding(W, S, k)triples wherek = range_a / Wwindows would need to be combined, and
determine_query_method()(
:155-173) picksDirect/Subtract/Mergebased on whether theunderlying sketch type supports subtraction or merging. None of that is
wired into the live query-serving path — it's only used by the offline
candidate_gen_dump/optimizer_clicost simulators. It's a reasonablestarting point for (b)'s design, not a ready-to-use implementation.
Secondary, smaller findings also relevant to (b)
cleanup.rs'sWindowType::Slidingbranch is single-window-shapedtoo (
planner/cleanup.rs:31-38): it hardcodes aread_count_thresholdof
1(ornum_stepsfor range queries), matching "a sliding query onlyever reads one window." Once multi-window merge exists, this needs to
become window-count-aware, like Tumbling's
lookback_bucketscalculationjust above it. It also skips the
CleanupPolicy::NoCleanupcheck that theTumbling branch has — a minor inconsistency worth fixing at the same time,
not urgent on its own.
Exact-match queries also have an unverified epoch/phase-alignment
dependency, independent of the merge gap:
simple_engine/mod.rs:419-423looks up a bucket at
[end_timestamp - window_size_ms, end_timestamp]exactly. The worker's buckets sit on a
slide_interval_ms-aligned gridfrom Unix epoch 0, but the query's own
end_timestampis only floor-alignedto
data_ingestion_interval_ms(the scrape interval) —promql.rs:44-56,align_end_timestamp_promql()— which knows nothingabout the aggregation's
slide_interval_ms. For queries fired on anarbitrary wall-clock schedule, whether
end_timestamplands on aslide-interval boundary is essentially down to when the experiment
happened to start, not something deliberately guaranteed. This matters
even in today's single-window-only case, and needs to be resolved as part
of (b)'s design (either by aligning to
slide_interval_msper-aggregation,or by changing exact-match to "nearest available window").
What "done" looks like for (b) (not designed yet)
A sliding-window query whose
data_range_msis a multiple ofwindow_size_ms(mirroring Tumbling's already-working rule) should be served correctly by
combining multiple stored sliding-window buckets — merge for mergeable
sketch types, subtract for subtractable ones (per
optimizer/sketch_properties.rs's existing mergeable/subtractableclassification) — the same way Tumbling already merges multiple non-overlapping
buckets today, but accounting for the overlap between sliding windows so
values aren't double-counted.
This needs its own
/grillingsession — deliberately not designed furtherhere.
Why this blocks the manual-override work
The
windowing: {type, slide_divisor}config override (see[[sliding-window-manual-override-plan]]) is otherwise ready to scope in
detail — global-only for now (with a separate tracked gap for per-
aggregation-id overrides, covering both this and the existing
sketch_parametersglobal-only limitation),slide_divisor-based slidesizing, PromQL + SQL paths, loud errors on non-exact division. But shipping
it before (b) lands means
windowing.type: slidingwould silently produceplans that fail at query-serving time for almost any real workload query —
a footgun, not a feature. Building the override is being held until (b)
ships.
fix(query-engine): warn instead of silently dropping keys with no pre…
fix(query-engine): warn instead of silently dropping keys with no pre…
fix(query-engine): merge all sliding-window buckets per key instead o…
milindsrivastava1997 commentedon Aug 25, 2026
554's execution is largely already done on
main, via a different route than the #557 stack (#562/#568) planned.#557's PR B (#568) proposed building sliding-multi-bucket merge as new work (extracting
merge_window_at_timestamp, reusing it for instant+range). Independently, five merged PRs already got both instant and range queries to correctly walk/merge multiple Sliding buckets by fixing adjacent bugs one at a time:HashMap(which silently dropped Sliding's legitimate multi-bucket case)slide_interval_ms, notwindow_size_ms(the grid buckets actually live on)So "implement merge across multiple stored sliding-window buckets" is now substantially satisfied on
mainalready, as fallout from general range-query bug fixes, not via the #557 stack. The #557-B (#568) approach should be re-verified against currentmainbefore reuse — it's likely obsolete, not just conflicting.What's still actually missing from 554's:
capability_matching.rs'swindow_compatible()— still harddata_range_ms == window_size_msexact-match onmain(capability_matching.rs:74). This is the literal gate keeping all of the above unreachable from real queries — nothing above changes 554's status until this is relaxed.cleanup.rs'sread_count_threshold = 1hardcoding — untouched.Also worth folding into scope: #588 (DeltaSetAggregator restricted to tumbling) requires the merge path above to special-case DeltaSetAggregator as tumbling-only even when co-located with sliding-capable accumulators in the same query — none of 554/557/588 currently assigns ownership of wiring that exception in.