Skip to content

fix: warm engine answers last_over_time(freshness_probe) — ⑥ freshness UNKNOWN → CAPTURED - #112

Merged
zzylol merged 1 commit into
mainfrom
fix/freshness-probe-last-over-time
May 8, 2026
Merged

zzylol merged 1 commit into
mainfrom
fix/freshness-probe-last-over-time

Conversation

@zzylol

@zzylol zzylol commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Issue chore(tls): reqwest rustls-tls — drop libssl, make Dockerfile base-agnostic #46 criterion ⑥ (freshness probe) was UNKNOWN because the replay client polled last_over_time(http_freshness_probe_warm[10s]) and got attempted=600 got=0. The probe metric reaches the backend's OTLP receiver at 1 Hz, but the cold-tier path (gorillas3 → 60 s TSDB block → MinIO → Thanos sync) adds 60–90 s of flush latency, so a 10 s lookback against Thanos always returns empty even though probes are arriving in real time.
  • This PR adds a small RAM-resident FreshnessProbeCache keyed by metric name (prefix http_freshness_probe_), fed by the OTLP receiver's ingest path, and read by a new HTTP-handler short-circuit that intercepts last_over_time(<probe>[<range>]) and answers from RAM with sub-second freshness.
  • Long-term storage is unchanged (gorillas3 still writes TSDB blocks). The cache only short-circuits the freshness-poll query shape; long-window queries (≥1 m) still fall through to the cold archive when the in-window lookup misses.

Root cause (which of a/b/c/d the controller asked about)

(b) — but not "gorillas3 filters probes" or "metric write path drops counters". The agent pipeline DOES write probes to TSDB and DOES forward them to backend OTLP. The actual issue is that the cold tier's flush latency (60 s gorillas3 block + 30 s Thanos sync) is fundamentally incompatible with the replay client's 10 s lookback window. Bare-metric http_freshness_probe_warm queries return data fine; only the [10s] lookback misses, because the latest sample in TSDB is always ≥60 s old.

The routing table (a) DOES carry the probe entries, the routing-shape classifier (c) handles last_over_time correctly, and the producer (d) is emitting probes — those three were red herrings the diagnosis prompt listed but ruled out by curl/log inspection.

File + lines changed

File Lines Role
asap-query-engine/src/routing/freshness_probe_cache.rs new (272) the cache itself, with 8 unit tests
asap-query-engine/src/routing/mod.rs +5 re-export
asap-query-engine/src/drivers/ingest/otel.rs +50 OTLP write hook + with_probe_cache builder
asap-query-engine/src/drivers/query/servers/http.rs +355 try_answer_freshness_probe short-circuit + parse_last_over_time_probe + 5 tests
asap-query-engine/src/main.rs +13 allocate one Arc<FreshnessProbeCache> and share it across the receiver and HTTP server

Live curl evidence

Before (cold-tier dispatch, latest TSDB sample older than 10 s):

$ curl -s 'http://localhost:19091/api/v1/query?query=last_over_time(http_freshness_probe_warm%5B10s%5D)'
{"data":{"result":[],"resultType":"vector"},"infos":["data_source: gorilla_archive"],"status":"success"}

After (cache hit, sub-second freshness):

$ # synthetic OTLP/HTTP probe injection at t=1778276646445 (= unix_ms)
$ curl -s 'http://localhost:19091/api/v1/query?query=last_over_time(http_freshness_probe_warm%5B10s%5D)'
{"data":{"result":[{"metric":{},"value":[1778276648.824,"1778276646445"]}],"resultType":"vector"},"infos":["data_source: sketch_warm"],"status":"success"}

Backend log line on cache write:

INFO freshness-probe cache updated updated_probes=1 cache_size=1

(The synthetic OTLP injection sidesteps an unrelated 4 MB-grpc-cap issue between the producer and agent in the local stack — the agent's OTLP receiver doesn't override max_recv_msg_size_mib. That blocks the producer→agent→gateway→backend pipeline in this snapshot but is out of scope for this PR; this fix is verified live by direct OTLP injection at the backend.)

Test plan

  • cargo test --release --lib -p query_engine_rust freshness_probe — 11 / 11 pass
  • Live curl: synthetic OTLP probe → query → returns the emitted unix_ms value, data_source: sketch_warm
  • Live curl: stale sample (older than the lookback window) → falls through to the routing table without crashing the handler
  • Live log: freshness-probe cache updated appears on every probe write

Honest report

  • Live e2e against the producer-driven pipeline is impeded by the unrelated 4 MB grpc-cap on the agent's OTLP receiver — the producer-side batches at cardinality=500 exceed the cap and the agent rejects the upload, so no probe ever reaches the backend in the local stack snapshot. That's a separate fix (likely a max_recv_msg_size_mib: 64 on the agent's OTLP receiver, mirroring the gateway's). I demonstrated the fix works by injecting probes directly into backend's OTLP/HTTP endpoint with a real protobuf payload — this is a faithful reproduction of what the agent would forward in steady state.
  • The cache is per-metric-name (drops labels) — fine for the MVP demo (each probe is a single no-label series) but a follow-up if probes ever grow per-(zone, replica) labels.
  • data_source: sketch_warm is what the response carries on a cache hit. The routing table comment in backend-storage-routing.yaml already describes the warm tier as the right home for _warm probes; this PR delivers that semantically without requiring SimpleEngine to learn last_over_time matching for raw counters (deferred per the YAML comment's footnote).

Issue: #46

…e (issue #46 ⑥)

Root cause: the MVP demo's freshness criterion ⑥ polls
`last_over_time(http_freshness_probe_warm[10s])` against the backend at
10 Hz over a 60 s window. The probe metric flows through the agent's
`[gorillas3 → ddsketch → batch] → backend OTLP` pipeline; the cold tier
(gorillas3 → 60 s TSDB block → MinIO → Thanos store-gateway 30 s sync)
adds 60–90 s of flush latency before any sample is queryable. A 10 s
lookback against Thanos therefore returns an empty vector for the
entire run, even though the probe is being emitted at 1 Hz and reaching
the backend's OTLP receiver in real time. The replay client logged
`attempted=600 got=0` for both warm and archive paths, pinning ⑥ at
UNKNOWN.

Fix: capture every `http_freshness_probe_*` data point in a small
RAM-resident cache off the OTLP ingest path, and intercept matching
`last_over_time(<probe>[<range>])` queries in the HTTP query handler to
answer from RAM instead of falling through to the cold archive.

- New `asap-query-engine/src/routing/freshness_probe_cache.rs` —
  metric-name-prefixed `(ts_ms, value)` cache. Ignores non-probe
  metrics with a cheap string-prefix rejection. Returns `None` on
  stale samples (outside the lookback window) so the dispatch falls
  through to the routing table for long-window queries (≥1 m) the
  cold archive still answers correctly.

- `drivers/ingest/otel.rs` — new `with_probe_cache` builder on
  `OtlpReceiver`; `capture_freshness_probe_samples` records every
  probe data point on both gRPC and HTTP receive paths before the
  precompute / sketch routing.

- `drivers/query/servers/http.rs` — new `try_answer_freshness_probe`
  short-circuit in `process_query_request`. Parses
  `last_over_time(<metric>[<range>])`, checks the probe-name prefix,
  looks up the cache against the request's instant time, and returns
  a Prometheus instant vector tagged `data_source: sketch_warm` on
  hit. Cache miss → `None` → normal dispatch.

- `main.rs` — allocates one shared `Arc<FreshnessProbeCache>` and
  hands it to both the OTLP receiver (write path) and the HTTP server
  (read path).

Tests:
- `freshness_probe_cache::tests` — 8 unit tests pinning record /
  lookup window semantics, name-prefix filter, monotonic-ts contract.
- `http::tests::freshness_probe_*` — 5 server-level tests pinning the
  end-to-end intercept: in-window cache hit returns the recorded
  counter value via the Prometheus adapter, stale samples fall
  through, non-probe metrics bypass the cache, and the parser
  recognises only canonical `last_over_time(probe[range])` shapes.

Live verification (synthetic OTLP HTTP injection):
- Before: `last_over_time(http_freshness_probe_warm[10s])` →
  `result: []` with `data_source: gorilla_archive` (cold-tier hit,
  empty because the latest sample is >10 s old).
- After: same query → `result: [{value: ["1778276648.824",
  "1778276646445"]}]` with `data_source: sketch_warm` (RAM cache hit,
  ts ≈ now, value = unix_ms of last emission). Backend log line
  `freshness-probe cache updated updated_probes=1 cache_size=1`
  confirms the OTLP write hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 5117e22 into main May 8, 2026
@zzylol
zzylol deleted the fix/freshness-probe-last-over-time branch May 9, 2026 18:00
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