fix(gorillas3): TSDB appender out-of-bounds with rotating-cardinality metrics - #354
Merged
Merged
Conversation
… metrics Root cause: Prometheus TSDB head appender locks `minValidTime` at `firstAppendedSampleTs - chunkRange/2` for the appender's lifetime. With Go's randomised map iteration, if the first-visited series in a flush window held samples in e.g. the [40..55s] slice, the floor became 10s and any subsequent series with samples at t<10s tripped `storage.ErrOutOfBounds`. PR#338's fake-exporter `unique_users_per_min` rotating user_id pool made this trip on every flush, crashing the agent 60s after start. Fix: flatten samples across all series, sort globally by timestamp ascending, then drive Append. Anchors `minValidTime` at the smallest ts in the window so every other sample lands above the floor. Also tolerates `ErrOutOfBounds` defensively (skip + count via new `gorillas3_tsdb_oob_samples_dropped_total` counter) so future late-arriving samples can't crash the agent. Coverage: TestTSDBBlockBuilder_RotatingCardinalityNoOOB (50 trials of randomized map iteration with non-overlapping per-series time slices), TestFlushTSDB_OOBDoesNotCrashProcessor (full processor flush path), TestTSDBBlockBuilder_WideTimestampSpanNoOOB (5-minute span tolerance). Closes #46. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
zzylol
added a commit
that referenced
this pull request
May 9, 2026
… (#46) (#355) Root cause: PR #353 spawned six gorillas3 instances (one per per-family pipeline under the 5-sketch routing topology), each with its own in-memory windowState. With the static placeholder's 60 s flush interval and PR #338's fake-exporter cardinality, the agent buffered the full per-pipeline working set in RAM BEFORE the first flush ticker fired — peak crossed the 1.5 GiB per-agent memory limit, the agent was OOM-killed, and zero TSDB blocks ever reached MinIO. PR #354's flat-slice OOB-tolerance approach compounded the problem at flush time (O(N_samples) flat slice + interleaved Head chunk creation across every series) but the agent never lived long enough for that path to fire. Result in /tmp/asap-mvp-rerun-bug34/asap/measurements/accuracy.csv: 1713 archive_miss / 0 archive_ok (was 343 archive_ok pre-#354). Two-part fix: 1) opentelemetry-collector-contrib-patch/processor/gorillas3processor: replace PR #354's flat-sort-and-skip path with a series-visit- ordered append. Each series' points are still sorted ascending locally, but series are now visited in ascending order of each series' EARLIEST sample timestamp. That guarantees the very first `app.Append(...)` carries the global minimum, anchoring the appender's `minValidTime = globalMin - chunkRange/2` so every other in-window sample passes the OOB check — without ever allocating an O(N_samples) flat slice or interleaving Head series creation. Memory footprint at flush is bounded by `max(series points, Head per-series state)` instead of total sample count. PR #354's defensive ErrOutOfBounds tolerance (`gorillas3_tsdb_oob_samples_dropped_total` counter + warn log, no rollback) is retained for genuinely late-arriving samples. 2) deploy/configs/asap-otel-agent-b6-asap-single-sketch.yaml: reduce gorillas3.window_interval from 60 s → 5 s. With six per-pipeline windowStates instead of one, the per-instance in-flight buffer is the dominant memory pressure. 5 s caps the per-pipeline window peak at ~150 MiB, putting the steady-state aggregate inside the agent's 1.5 GiB budget. tsdb_block_duration stays at 60 s so the on-S3 layout still matches the Thanos store-gateway sync interval; flushes just roll smaller sub-blocks that thanos-compact will merge. Verification (90 s soak diagnose, repeated): before: mc ls myminio/asap-gorilla-tsdb/ → empty curl -s http://localhost:19092/api/v1/labels → {"data":["__name__"]} (no metric labels surfaced) after: mc ls myminio/asap-gorilla-tsdb/ → 27 blocks across all six pipelines (countsketch_path, countminsketch_path, ddsketch_path, hll_path, kll_path, raw_passthrough) curl -s http://localhost:19092/api/v1/label/__name__/values → ["endpoint_request_freq","http_freshness_probe_archive", "http_freshness_probe_raw","http_freshness_probe_warm", "http_requests_total","http_requests_total_latency_ms", "request_size_bytes","top_endpoint_qps", "unique_users_per_min"] all 5 sketched metrics + raw + 3 freshness probes. Coverage: TestTSDBBlockBuilder_HighCardinalityMemoryBound — 200-series / 60-sample window with non-overlapping per-series time slices, so the global minimum lives on a different series from the global maximum. Asserts 0 OOB drops + every sample lands in the block. Pre-this-PR's algorithm would still have passed correctness but allocated the O(N) flat slice; this test pins the visit-order invariant going forward. Existing TSDB OOB regressions (RotatingCardinalityNoOOB, WideTimestampSpanNoOOB, FlushTSDB_OOBDoesNotCrashProcessor) all still pass with the new visit-order path — same OOB-tolerance contract, different memory footprint. Closes #46. 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.
Bug
Agent processor crashed 60s after start with:
Reproducible 100% of the time after PR #338's high-cardinality rotating metrics (
unique_users_per_min,top_endpoint_qps) started flowing. Blocks the entire MVP demo.Root cause (hypothesis (a) — timestamp ordering)
Prometheus TSDB's
head.Appender()snapshotsminValidTime = appendableMinValidTime() = max(MaxTime - chunkRange/2, headMinValid)at the time the appender is created (or, for an empty Head, on first Append). For a freshBlockWriterHead this meansminValidTime = firstAppendedSampleTs - chunkRange/2— fixed for the appender's lifetime.With a 60s
tsdb_block_durationand Go's randomised map iteration over the window, if the first-visited series held samples in e.g. the[40..55s]slice of the window, the floor locked at10s = 40 - 30. Any subsequent series with samples att<10sthen trippedstorage.ErrOutOfBounds.PR #338's
unique_users_per_minrotates user_ids over the window — differentuser_idseries have NON-OVERLAPPING per-series timestamp ranges by design — which made cross-series-ordering OOB fire on every flush.Fix
opentelemetry-collector-contrib-patch/processor/gorillas3processor/tsdb_block_writer.goappendWindow: flatten every(labelset, ts, v)tuple across all series, sort globally by timestamp ascending, then driveapp.Append. Guarantees the first appended sample is the smallest ts in the window, sominValidTime = min(window) - 30ssits below every other sample. (The per-series ascending sort is preserved as a stable secondary order.)errors.Is(err, storage.ErrOutOfBounds)fromAppendis now NON-fatal — the sample is skipped, a counter is bumped, and the appender continues. The artifact surfaces aNumOOBDroppedcount.selfmonitor.go: newgorillas3_tsdb_oob_samples_dropped_totalcounter so operators see drift.processor.goflushTSDB: log aWarn(notError) per-flush when drops occur; bump the counter.Test coverage
tsdb_block_writer_test.go:TestTSDBBlockBuilder_RotatingCardinalityNoOOB— 50 trials of randomised map iteration over 8 series with non-overlapping 7-second slices in a 60s window. Fails on the unfixed code with the exact error from production; passes on the fix every iteration.TestFlushTSDB_OOBDoesNotCrashProcessor— drives the same scenario through the fullConsumeMetrics → flushWindow → flushTSDB → PutTSDBBlockpath; round-trips the block viatsdb.OpenBlockand verifies all 56 samples land.TestTSDBBlockBuilder_WideTimestampSpanNoOOB— 5-minute span between earliest and latest samples in a single window builds cleanly.Before/after evidence
Reproducer on baseline (unfixed) main:
After fix:
Closes #46 (gorillas3 TSDB OOB blocker).
Test plan
🤖 Generated with Claude Code