Skip to content

fix(data_plane): bound warm SketchStore memory with retention horizon - #327

Merged
zzylol merged 1 commit into
mainfrom
fix/sketchstore-retention
May 25, 2026
Merged

zzylol merged 1 commit into
mainfrom
fix/sketchstore-retention

Conversation

@zzylol

@zzylol zzylol commented May 25, 2026

Copy link
Copy Markdown
Contributor

Root cause (the leak)

asap-data-plane RSS grew ~0.74 GiB/min unbounded under steady OTLP sketch ingest, freezing a 251 GiB node. The [MEMORY_DIAG] showed a flat ~20218 sid count yet climbing RSS and 0.00 KB sealed — so the growth is per-instance, in current_epoch.

  • SketchStore::append_sample/append_precompute always build the per-sid store via SidStoreData::new(), which sets epoch_capacity: None (index/epoch_columnar.rs). maybe_rotate_epoch() therefore returns early on the very first match arm — nothing ever seals (hence 0.00 KB sealed).
  • The persistence flusher (persistence/flusher.rs) and approx_memory_bytes only ever operate on sealed_epochs (list_sealed_epochs -> evict_sealed_epoch). With nothing sealed, the eviction/retention path never sees current_epoch at all.
  • lifecycle/eviction.rs (SchemaEvictionService) only drops whole Expired sids (schema retirement). In steady state sids stay Active forever, so it never fires.

Net: current_epoch's columnar Vecs accumulate one ~19 KB pane per 30s tumbling window per sid forever — memory was O(active_series × total_elapsed_time). Two unbounded sources: (1) nothing seals, (2) even sealed epochs only evict by count (max_epochs), never by age for current_epoch.

Fix

A time-based retention horizon on SidStoreData:

  • On each insert, evict windows whose END is older than newest_end − horizon from current_epoch (and drop fully-behind sealed_epochs). Memory becomes O(active_series × horizon).
  • Configurable via env ASAP_SKETCH_RETENTION_MS (0 disables), resolved once via OnceLock off the hot path. Default 2h (DEFAULT_SKETCH_RETENTION_MS).
  • This is retention/eviction, not force-seal-and-read-only-sealed: recent unsealed windows stay queryable via the overlap scan.

Why it can't regress #323#326

The 2h horizon comfortably exceeds the ~30m max range-query window plus the delta-stitching carry-in's Full-base reach. Eviction keys on window-END, so a pane straddling the cutoff survives until fully behind the horizon — the overlap scan (range_query_overlap_into) and carry-in (collect_ending_at_or_before) still see everything within the horizon. Older data lives in the cold/raw tier.

Tests

cargo test -p data_plane — 739 lib tests pass (0 fail). New regression tests:

  • evict_window_ends_before_* — drops old / keeps straddling+recent, recomputes bounds, no-op fast path.
  • retention_bounds_window_count_over_long_elapsed_time — 4h of 30s panes stays bounded to ~horizon/window (not 480).
  • retention_disabled_keeps_full_historyNone horizon = legacy behavior.
  • retention_keeps_windows_within_horizon_queryable + e2e retention_bounds_memory_yet_keeps_recent_windows_queryable — 30m range query within horizon still resolves with its Full carry-in base intact.

cargo clippy -p data_plane — no new errors/warnings in the changed files.

🤖 Generated with Claude Code

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
zzylol merged commit 5eb1a27 into main May 25, 2026
@zzylol
zzylol deleted the fix/sketchstore-retention branch May 25, 2026 13:32
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