fix(data_plane): make durable sketch tier flush, read exact-agg from disk, report real memory - #330
Merged
Merged
Conversation
…agg from disk, and report real memory PR #329's durable tier passed its unit tests but the first live run (--persistence-seal-window-count=4 --persistence-hot-window-secs=120) left parts/ empty after 13 min of ingest, lost all data on docker restart, dropped [5m]/HLL queries under persistence, and reported ~0 KB sealed bytes. Three root causes: 1. Flush never fired (most severe). The flusher only ever flushes SEALED epochs, and sealing only fires on the count cadence (seal_window_count distinct windows). A slow/stalled series never reaches the cadence, so its aged windows sit un-sealed in current_epoch forever — never made durable. Fix: a time-driven "phase 0" seal — the flusher now rolls every current_epoch window older than the hot window into a sealed epoch each tick (EpochSource::seal_aged_epochs / SidStoreData::seal_aged_windows / MutableEpoch::split_window_ends_before) so it becomes flushable regardless of cadence. Parts now commit during runtime and survive restart. 2. Exact-agg disk read-back missing. query_exact_agg_range and exact_agg_coverage_bounds read only in-memory epochs, so a `sum by (...)` / rate query returned "No result" once its windows were flushed-then-evicted. Fix: both now union the durable tier, reconstructing scalar accumulators (Sum/Increase/MinMax + Multiple*) from disk via reconstruct_exact_agg, keyed by the rebuilt label map. 3. approx_memory_bytes ignored current_epoch, so the MEMORY_DIAG under-reported and the flusher's memory-pressure trigger was blind to the bulk of memory (which under persistence lives un-sealed in current_epoch). Fix: count hot current_epoch + sealed; relabel the diagnostic. Persistence-OFF default path is unchanged (seal_aged is a no-op when persistence_enabled is false; the disk unions are no-ops without a read handle). Reproducing tests fail on origin/main and pass here: live_aged_unsealed_panes_flush_and_survive_restart (#1), live_exact_agg_resolves_from_disk_after_evict (#2), live_total_memory_accounts_for_current_epoch (#3), plus columnar/seal and flusher-level unit tests. Remaining follow-up: MultipleMinMaxAccumulator (needs an out-of-band min/max sub_type) and the sketch-backed accumulator forms still have no generic byte factory, so their evicted-to-disk exact-agg portion is skipped; they remain served from memory. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged
3 tasks
zzylol
added a commit
that referenced
this pull request
May 25, 2026
…yable (#332) After a data-plane restart the durable warm-sketch tier recovered the parts manifest + part cache (#329/#330) but registered NO sids in the in-memory SketchStore `instances` map -- registration only ever happens on the live ingest path when a fresh DataPoint arrives. With an empty registry, `instances_matching` enumerated nothing for the recovered metrics (engine returned "No result" before reading any window) and `query_range`/`query_exact_agg_range`'s disk-union early-returned on the missing `sid_group_by_keys`. The on-disk part format carries only label VALUES + sketch_type_name -- not the metric name, group-by KEYS, or structured `AggKind` the query path needs. Fix: persist a compact per-sid metadata sidecar (`sid_metadata.json`) that the flusher upserts whenever it makes a part durable, and replay it on recovery to re-register each disk-resident sid as a queryable instance. `capability`/`accuracy` are re-derived from the persisted `agg_kind` exactly as the ingest path derives them. Idempotent: a sid a live DataPoint already re-registered is not clobbered. Persistence-OFF behavior is unchanged (the sidecar only exists under the flusher). #330's restart tests passed despite this bug because they call `idx2.register(...)` on the fresh store before querying ("here we re-register to model that") -- masking the disk-only path. The two new tests do a GENUINE fresh reopen with NO register() for both the KLL quantile and Sum exact-agg shapes; both fail on origin/main and pass with this fix. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
PR #329's durable tier passed unit tests but the first live run
(
--persistence-seal-window-count=4 --persistence-hot-window-secs=120)exposed three bugs the tests missed. On node2, after 13 min of ingest the
sketch_index/parts/dir was empty,parts_manifest.logwas 0 bytes andthe snapshot was a freshly-initialized empty manifest — only
series_resolver.walgrew (1.5M). So data flowed but nothing was ever madedurable, a
docker restartrecoveredlive=0, and post-restart queriesreturned "No result".
Bug 1 — flush never fired (most severe). file:
persistence/flusher.rsThe flusher only flushes sealed epochs, and sealing only fires on the
count cadence (
seal_window_countdistinct windows incurrent_epoch,epoch_columnar.rs::maybe_rotate_epoch). A slow/stalled series never reachesthe cadence, so its aged windows sit un-sealed in
current_epochforeverand are never flushable. Fix: a time-driven "phase 0" seal — each tick the
flusher rolls every
current_epochwindow older than the hot window into asealed epoch (new
EpochSource::seal_aged_epochs→SidStoreData::seal_aged_windows→MutableEpoch::split_window_ends_before),so it becomes flushable regardless of cadence. Parts now commit during runtime
and survive restart.
Bug 2 — exact-agg disk read-back missing. file:
index/mod.rsquery_exact_agg_range/exact_agg_coverage_boundsread only in-memoryepochs, so a
sum by (...)/ratequery returned "No result" once itswindows were flushed-then-evicted. Fix: both now union the durable tier,
reconstructing scalar accumulators (Sum/Increase/MinMax + Multiple*) from disk
via
reconstruct_exact_agg, keyed by the rebuilt label map.Bug 3 —
approx_memory_bytesignoredcurrent_epoch. file:index/mod.rs+main.rsIt summed sealed epochs only, so the
MEMORY_DIAGunder-reported ("0.00–0.12KB approx sealed bytes") and the flusher's memory-pressure trigger was
blind to the bulk of memory (which under persistence lives un-sealed in
current_epoch). Fix: count hotcurrent_epoch+ sealed; relabel thediagnostic.
The persistence-OFF default path (#327) is unchanged:
seal_aged_epochsis ano-op when
persistence_enabledis false, and the disk unions are no-opswithout a read handle.
Reproducing tests (fail on origin/main, pass here)
live_aged_unsealed_panes_flush_and_survive_restart— bug 1: agedbelow-cadence panes flush to disk and survive a reopen of the same dir.
live_exact_agg_resolves_from_disk_after_evict— bug 2:sum by (zone)shape + coverage bounds resolve from disk after flush+evict.
live_total_memory_accounts_for_current_epoch— bug 3: memory accountingincludes un-sealed
current_epoch.split_window_ends_before_*,seal_aged_windows_*,hot_window_time_seals_unsealed_aged_epochs_then_flushes.Test plan
cargo test -p data_plane— 756 lib + integration pass, 0 failcargo clippy -p data_plane— exit 0, no new warnings in changed files(pre-existing
approx_constantat forward.rs:850 unchanged)ingest, then
docker restartthe data-plane and re-run the marqueequeries.
Recommended live re-validation (forced-flush) settings
--persistence-enabled --persistence-dir=... --persistence-seal-window-count=4 --persistence-hot-window-secs=30 --persistence-flush-interval-ms=1000. A30s hot window (below the 120s used originally) makes the time-driven seal
fire promptly so flushing is observable within ~1 min; expect non-empty
parts/+ a growingparts_manifest.log, andMEMORY_DIAGshould now showreal in-memory bytes. Then
docker restartand confirmpersistence recovery: live=N>0and thequantile_over_time(...[30m])queryreturns data.
Remaining follow-up
MultipleMinMaxAccumulator(needs an out-of-band min/maxsub_typenot storedin the part) and the sketch-backed accumulator forms have no generic byte
factory, so their evicted-to-disk exact-agg portion is skipped in
reconstruct_exact_agg— they remain served from memory. Sketch query_rangeread-back (KLL/HLL/DDSketch as opaque bytes) is already disk-aware and
unaffected.
🤖 Generated with Claude Code