Skip to content

fix(data_plane): overlap-scan sketch reads for short/instant windows - #326

Merged
zzylol merged 1 commit into
mainfrom
fix/sketch-short-delta-window-and-instant
May 25, 2026
Merged

zzylol merged 1 commit into
mainfrom
fix/sketch-short-delta-window-and-instant

Conversation

@zzylol

@zzylol zzylol commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the final two warm sketch-query gaps after #323/#324/#325.

SketchStore::query_range scanned epochs with strict containment
(w.0 >= start && w.1 <= end), but the agent emits ~30s tumbling panes.
A query window narrower than one pane cadence can't fully contain any
pane, so the scan returned zero in-window samples → the #325 carry-in
(which keys off the earliest in-window sample) never fired → empty
series surfaced as "No result for query".

  • Gap 1quantile_over_time(...[30s]) → "No result" ([45s]+
    worked: a 45s+ window can contain a 30s pane).
  • Gap 2 — bare/instant http_requests_total_latency_ms /
    quantile(...) → empty: per-window family + the straddling freshest
    pane invisible, so the instant projection (samples.last()) found
    nothing.

Fix

  • Add range_query_overlap_into to MutableEpoch + SealedEpoch
    (half-open overlap w.1 > start && w.0 < end, matching the overlap
    semantics range_query_into_grouped already documents for this exact
    30s-pane case).
  • Switch only the sketch query_range reads to overlap. Exact-agg /
    rate / sum by paths keep containment → working shapes can't regress.
  • The reducer's existing w_end >= t0 per-window filter and cumulative
    latest_end projection still drop out-of-range values, so no carry-in
    / straddling-pane value leaks into the answer's time domain.

Actual KLL ingest encoding

otel.rs assigns each datapoint's SketchEncoding straight from the
OTLP dp.encoding field via encoding_to_handle, defaulting unset
(0) to ProtoFull (otel.rs:1267-1268). KLL deltas (ProtoDelta) are
applied as mergeable Full fragments through decode_full
(delta_apply.rs:157-171). The carry-in's need_base correctly matches
ProtoDelta | MsgpackDelta; the bug was upstream — containment starved
it of in-window samples.

Test plan

  • cargo test -p data_plane — 733 lib + integration tests pass, 0 failures
  • New epoch-level overlap-vs-containment + half-open boundary tests
  • New reducer KLL short-window cumulative + per-window-instant tests (real proto bytes)
  • New wide-window cumulative non-regression test ([2m]+ unchanged)
  • cargo clippy -p data_plane — 0 new issues in changed files (one pre-existing approx_constant error in untouched forward.rs:850 remains on main)

🤖 Generated with Claude Code

The sketch read path (`SketchStore::query_range`) used strict CONTAINMENT
(`w.0 >= start && w.1 <= end`) when scanning epochs, but the agent emits
~30s tumbling panes. A query window narrower than one pane cadence — a
`quantile_over_time(...[30s])` range, or any instant selector whose
freshest pane straddles `now` — fails to FULLY CONTAIN any pane, so the
scan returned zero in-window samples. The #325 delta-stitching carry-in
keys off the earliest in-window sample, so with no in-window samples it
never fired, and the reducer yielded an empty series the engine surfaced
as "No result for query".

Two live gaps, one root cause:
  1. `quantile_over_time(...[30s])` -> "No result" ([45s]+ worked because
     a 45s+ window can contain a 30s pane).
  2. bare/instant `http_requests_total_latency_ms` / `quantile(...)` ->
     empty: per-window family + the straddling freshest pane invisible,
     so the instant projection (`samples.last()`) found nothing.

Add `range_query_overlap_into` to MutableEpoch + SealedEpoch (half-open
overlap `w.1 > start && w.0 < end`, matching the overlap semantics
`range_query_into_grouped` already documents for this exact 30s-pane
case) and switch only the sketch `query_range` reads to it. Exact-agg /
rate / sum paths keep containment, so the working `sum by` shapes can't
regress. The reducer's existing `w_end >= t0` per-window filter and
cumulative `latest_end` projection still drop out-of-range values, so no
carry-in / straddling-pane value leaks into the answer's time domain.

Regression tests lock in both the fix and non-regression: epoch-level
overlap-vs-containment + half-open boundary tests; reducer-level KLL
short-window cumulative + per-window-instant tests (real proto sketch
bytes through the live decode path); and a wide-window cumulative test
asserting the already-working `[2m]+` shape is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 6a4764a into main May 25, 2026
@zzylol
zzylol deleted the fix/sketch-short-delta-window-and-instant branch May 25, 2026 11:58
zzylol added a commit that referenced this pull request May 25, 2026
…#327)

The warm sketch store grew unbounded (~0.74 GiB/min) under steady OTLP
ingest, freezing a 251 GiB node. Root cause: SidStoreData is always
built with epoch_capacity=None, so maybe_rotate_epoch returns early and
nothing ever seals; the persistence flusher only evicts SEALED epochs,
so current_epoch accumulated one ~19 KB pane per 30s tumbling window
per sid forever. Memory was O(active_series x total_elapsed_time).

Add a time-based retention horizon on SidStoreData: on each insert,
evict windows whose END is older than newest_end - horizon from
current_epoch (and any sealed_epochs), making memory
O(active_series x horizon). Configurable via ASAP_SKETCH_RETENTION_MS
(0 disables), default 2h.

Does not regress the #323-#326 sketch read path: the horizon (2h)
comfortably exceeds the ~30m max range-query window plus the
delta-stitching carry-in's Full-base reach, and eviction keys on
window-END so a straddling pane survives until fully behind the
horizon. Recent unsealed windows stay queryable via the overlap scan
(no force-seal). Regression tests prove (a) long-elapsed ingest keeps
per-sid window count bounded and (b) a 30m range query within the
horizon still resolves with its Full carry-in base intact.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 25, 2026
Compose hot (current_epoch) -> sealed (in-mem, pending flush) -> disk
parts into a single tiered store so warm-sketch memory is bounded by
flush-then-evict rather than #327's age-based drop. Reads union all
three tiers across the requested range.

- Sealing now fires under persistence: SidStoreData gains a
  seal_window_count cadence (default 20 windows ~= 10 min of 30s panes)
  so current_epoch rotates into sealed_epochs for the flusher to
  persist. max_epochs drop is disabled under persistence (the flusher
  owns sealed-epoch lifecycle). In-memory-only deploys keep #327.
- query_range/union_disk_parts_into consult PartCache+Manifest for the
  evicted portion of the range, rebuilding the full label key->value
  map from the sid's group_by_keys and preserving the #323-#326 read
  contract (half-open overlap + delta-stitching carry-in) across the
  in-mem/on-disk boundary -- incl. a carry-in Full base that now lives
  on disk. Part format round-trips SketchEncoding via a repurposed v1
  pad byte (legacy 0 decodes as Full).
- enforce_retention is a no-op under persistence so retention never
  drops un-flushed sealed/current data; the disk-tier TTL bounds the
  durable copy. In-memory-only path is unchanged.
- start_persistence installs a read handle + seal cadence and recovers
  the manifest+parts so a restart immediately serves recovered data.
- New CLI flag --persistence-seal-window-count plumbs the cadence.

Tests: seal-fires, flush+evict bounds memory, query-from-disk incl.
disk carry-in base, restart recovery, and persistence-disabled
non-regression.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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