feat(data_plane): durable disk-backed tier for warm SketchStore - #329
Merged
Merged
Conversation
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>
Merged
3 tasks
zzylol
added a commit
that referenced
this pull request
May 25, 2026
…agg from disk, and report real memory (#330) 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
Turns the warm
SketchStore's half-wired persistence layer into a real durable tiered store: hot data in memory, older sealed data flushed to disk and queryable from disk, surviving restarts. Memory is now bounded by flush-then-evict under--persistence-enabled, instead of #327's age-based drop (which still bounds the in-memory-only default). Builds on #327 + #323–#326 — does not revert any of them.Tiered design
query_rangeunions all three across the requested range.SidStoreDatagains aseal_window_countcadence (default 20 distinct windows ≈ 10 min of 30s panes — large enough to amortize per-part header/index overhead, small enough that data behind the flusher'shot_windowis actually sealed and thus flushable).current_epochrotates intosealed_epochson cadence so the flusher has sealed epochs to persist. Themax_epochsrotation-drop is disabled under persistence (the flusher owns sealed-epoch lifecycle: seal → durable part → evict). With persistence OFF, nothing seals → fix(data_plane): bound warm SketchStore memory with retention horizon #327 retention bounds memory (unchanged).union_disk_parts_intoconsultsManifest::live_parts_overlapping+PartCachefor the evicted portion of the range. The fix(data_plane): canonicalize sketch metric name at OTLP ingest #323–fix(data_plane): overlap-scan sketch reads for short/instant windows #326 read contract is preserved across the in-mem/on-disk boundary: same half-open overlap (end_ts > start && start_ts < end), and the delta-stitching carry-in fetches the most-recent Full base from disk when it has aged out of memory. The full label key→value map is rebuilt from the sid'sgroup_by_keys(sorted) zipped against the stored values vector — no part-format change for keys.SketchEncoding(Full vs Delta) is round-tripped, via a repurposed v1 part-entry pad byte (legacy0decodes as Full, the safe carry-in default).enforce_retentionis a no-op so retention never drops un-flushed sealed/current_epochdata out from under the flusher; the disk-tier TTL (delete_older_than_ms) bounds the durable copy. When OFF, fix(data_plane): bound warm SketchStore memory with retention horizon #327 retention stays the bound. Composition is explicit and tested both ways.start_persistencerunsrecovery::recover, installs aPersistenceReadHandle(recovered manifest + cache) and the seal cadence, then starts the flusher — a reopened store on the same dir immediately serves recovered data.Deploy flags (operator)
Volume: mount a persistent volume at
--persistence-dir(parts + manifest live under<dir>/sketch_index/; the series-resolver WAL under<dir>/series_resolver.wal).Files changed
index/epoch_columnar.rs—SidStoreData::{seal_window_count, persistence_enabled};maybe_rotate_epoch(cadence + no-drop under persistence);enforce_retention(no-op under persistence).index/mod.rs—PersistenceReadHandle;SketchStore::{persistence_read, seal_window_count, fresh_sid_store, enable_persistence_mode, union_disk_parts_into, sid_group_by_keys, rebuild_label_map, list_sealed_epochs_len};query_rangedisk union;encoding_to_tag/tag_to_encoding;snapshot_sealed_epochrecords encoding.persistence/{config,part,source}.rs—seal_window_countconfig; per-entryencoding_taground-trip;encoding_tagconstants.main.rs—--persistence-seal-window-countplumbed into the config.Test plan
cargo test -p data_plane --lib— 748 passed, 0 failedcargo clippy -p data_plane --lib— no new errors (pre-existingapprox_constantatthanos_query_engine/forward.rs:850unchanged)Follow-ups (not in this PR)
query_range). The exact-agg / precompute disk read (query_exact_agg_range,query_precomputes_by_agg) is left in-memory-only — those payloads aren't part of the marquee sketch queries; extend with the sameunion_disk_parts_intopattern when needed.if letblock inquery_rangeto minimize diff (repo is not rustfmt-clean on main); a follow-up reindent is cosmetic.🤖 Generated with Claude Code