Skip to content

fix(data_plane): stop signature reconcile from retiring sketch sids (warm quantile/cardinality miss) - #324

Merged
zzylol merged 1 commit into
mainfrom
fix/sketch-query-policy-match
May 25, 2026
Merged

zzylol merged 1 commit into
mainfrom
fix/sketch-query-policy-match

Conversation

@zzylol

@zzylol zzylol commented May 25, 2026

Copy link
Copy Markdown
Contributor

Remaining root cause (after #323)

#323 stripped the _<family> suffix from stored sketch metric names — necessary but not sufficient. Tracing quantile_over_time(0.99, http_requests_total_latency_ms[30s]) end to end on the live node2 data-plane (recreated with data_plane::*=debug) showed the query routes correctly to SketchStore (shape=Quantile) then returns CapabilityMiss → "No result for query".

/api/v1/db/schemas for the failing metric showed a MIX of sids:

  • ~76 Sketch{Kll} group_by=[node,pod,producer_id,rack,zone]retired
  • ~24 Sketch{Kll} (same shape) — active (carrying live data; timeline confirms one active sketch segment)
  • 1 ExactAgg{DatasketchesKLL} group_by=[] — active (a precompute artifact)

Two compounding defects:

  1. reconcile_from_streaming_config wrongly retires every sketch sid. Its live-signature set comes only from signature_from_agg_config, which canonicalizes every streaming-config AggregationConfig to an AggKind::ExactAgg (P-prefixed) signature. An OTLP-ingested AggKind::Sketch sid always has an S-prefixed signature, so it can never be in the live set → it is orphaned and retired on the first reconcile. The module header even claims sketch sids "are never retired by this path" — the implementation does the opposite. Retirement bars ingest (§6.3 barrier), eviction drops the per-window state, and the sid becomes a query-time Ghost.

  2. The engine short-circuits the whole query on the first Ghost sid. engine.rs resolves a candidate to its sid set via instances_matching (empty group_by ⊆ everything ⇒ all 105 sids match) and iterates in ascending-u64 order. The older retired-then-evicted Ghost sids are hit first and abort the query to CapabilityMiss before reaching the newer Active Hit sids that hold live data.

Fix

  • lifecycle/reconcile.rs: skip AggKind::Sketch { .. } instances in the orphan scan (sketch-sid lifecycle is the control plane's eviction RPC's job, matching the documented intent).
  • query_engines/asap_query_engine/engine.rs: skip Ghost/Unknown sids instead of aborting; only fail over to archive when no Hit sid satisfies the capability (the existing hit_sids.is_empty() check preserves the all-ghost → CapabilityMiss contract, so the execute_returns_capability_miss_when_classify_is_ghost test still holds).

Tests

  • reconcile::tests::sketch_sids_are_never_retired_by_signature_reconcile
  • outer_agg_integration_tests::ghost_sid_does_not_mask_active_hit_sid_for_quantile

cargo build -p data_plane green; targeted cargo test -p data_plane green: reconcile (10), asap_query_engine (31, incl. the preserved ghost-miss test), sketch_db (229), ingest (40).

Test plan

  • After image rebuild + redeploy, confirm quantile_over_time(0.99, http_requests_total_latency_ms[30s]), quantile_over_time(0.99, request_size_bytes[30s]) (KLL) and count(unique_users_per_min) (HLL) return data_source: asap_query with values.
  • Confirm /api/v1/db/schemas keeps the active Sketch{Kll}/Sketch{Hll} sids Active across a control-plane config push.

NOT merged, NOT redeployed per task scope.

🤖 Generated with Claude Code

The per-ingest-batch sid reconcile (`reconcile_from_streaming_config`)
builds its live-signature set solely from `signature_from_agg_config`,
which canonicalizes every streaming-config `AggregationConfig` to an
`AggKind::ExactAgg` (`P`-prefixed) signature. An OTLP-ingested sketch
sid (`AggKind::Sketch`: KLL / HLL / DDSketch / CMS / CountSketch) always
produces an `S`-prefixed signature, so it can NEVER be present in the
live set — meaning the scan unconditionally orphaned and retired EVERY
sketch-backed sid on the first config reconcile. The module header
already documents the intended behavior ("sketch sids never compare
equal so they're never retired by this path"); the implementation did
the opposite.

Retiring a sketch sid bars further ingest (the §6.3 write barrier),
eviction then drops its per-window state, and the query path
classifies it as `Ghost`. The analyzer-driven dispatch in the asap
query engine iterates a candidate's sid set in ascending-u64 order and
short-circuited the WHOLE query to CapabilityMiss on the FIRST
Ghost/Unknown sid — so an older retired-then-evicted sketch sid masked
the freshly-minted Active sketch sid that carried live data. This is
why warm `quantile_over_time(...)` (KLL) and `count(...)` (HLL) queries
returned "No result for query" even after #323 fixed the metric-name
layer.

Two fixes:
1. reconcile.rs: skip `AggKind::Sketch` instances in the orphan scan.
   Sketch-sid lifecycle is the control plane's eviction RPC's job.
2. engine.rs: skip Ghost/Unknown sids instead of aborting the query;
   only fail over to archive when no Hit sid satisfies the capability
   (the `hit_sids.is_empty()` check preserves the all-ghost contract).
   Hardens against legitimately merged-away ghost sketch sids that
   coexist with active ones.

Tests: `sketch_sids_are_never_retired_by_signature_reconcile` and
`ghost_sid_does_not_mask_active_hit_sid_for_quantile`.

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