Skip to content

refactor(data_plane): rekey ingest bucketing from (agg_id, group_key) to sid (B7.6) - #284

Merged
zzylol merged 1 commit into
mainfrom
b7.6-ingest-bucketing-sid
May 18, 2026
Merged

zzylol merged 1 commit into
mainfrom
b7.6-ingest-bucketing-sid

Conversation

@zzylol

@zzylol zzylol commented May 18, 2026

Copy link
Copy Markdown
Contributor

Scope

Schema retirement #5 step 6 (issue #272). The precompute engine's per-bucket state used to be keyed by (agg_id, group_key). The grouping label values already fold into the sid via SeriesIdResolver's (metric, attrs_fingerprint, agg_kind_canonical) identity contract, so the tuple collapses to a single u64sid is now the bucket key throughout the ingest → router → worker path.

WorkerMessage shape change (BREAKING within the data_plane crate)

WorkerMessage::GroupSamples / AccumulatorInput carry sid: u64, policy_fp: PolicyFingerprint, group_key: String in place of the old agg_id + group_key pair.

  • sid — registry-allocated bucket identity; folds in (metric, attrs_fingerprint, agg_kind_canonical). Worker keys group_states on this.
  • policy_fp — source AggregationConfig fingerprint. Worker looks up its AggregationConfig via snap.get_aggregation_config(policy_fp.as_u64()).
  • group_key — kept for emit-time KeyByLabelValues rendering.

WorkerMessage is pub but only used inside the data_plane crate (the precompute engine's private dispatch contract); no external crate consumes it.

Files touched

  • data_plane/src/precompute_engine/series_router.rs — message shape + routing hash (worker_for_sid replaces worker_for_group) + test renamed to test_consistent_sid_routing.
  • data_plane/src/precompute_engine/worker.rsGroupState / Worker.group_states retyped to HashMap<u64, _>; get_or_create_group_state / process_group_samples / process_accumulator_input / evict_orphaned_groups / flush_all reworked; 30+ test call sites updated to pass (sid, PolicyFingerprint, group_key).
  • data_plane/src/drivers/ingest/otel.rs — all three OTLP dispatch paths switched to sid-bucketing via new helper resolve_bucket_sid_for_agg_config(state, config, point_labels). Crucially, "attrs" for sid resolution is the GROUPING-LABEL projection of wire labels (not the full label set), so distinct (rack, node, pod) tuples under a grouping_labels=[zone] policy still roll up into one bucket per zone (the GROUP-BY semantic).

Test added

drivers::ingest::otel::sid_bucketing_tests::raw_otlp_buckets_by_sid_with_distinct_group_keys — drives route_otlp_to_precompute end-to-end with two zone values × two DPs each, asserts:

  • exactly two WorkerMessage::GroupSamples are emitted
  • their sids are non-zero and distinct
  • each sid equals what SeriesIdResolver::lookup records for (metric, "zone=<zv>;", ExactAgg-canonical)
  • policy_fp = config.aggregation_id() on every message
  • samples in each bucket are exactly the DPs whose zone matches that bucket

The test docstring documents a pre-existing format_series_key / parse_labels_from_series_key inconsistency that makes extract_group_key_for return "" for OTLP inputs; B7.6 bucketing is unaffected because it reads point.labels directly (HashMap lookup), not the joined series_key.

Test plan

  • cargo build -p data_plane — clean
  • cargo test -p data_plane --lib — 713 passed / 2 ignored, no regressions vs. main
  • cargo test -p data_plane integration suite — same 2 pre-existing failures as origin/main (controller_plan_to_query_full_roundtrip_ddsketch / _kll, both reproducible on a fresh origin/main worktree). Unrelated to B7.6.
  • New regression test passes

NOT in scope (B7.7's domain)

  • output_sink.rs already reads output.policy_fp (no agg_id in PrecomputedOutput); no changes needed there.
  • backfill/processor.rs still uses (agg_id, group_key) internally — B7.7.
  • AggregationConfig::aggregation_id() accessor remains; retiring it is deferred until B7.6 + B7.7 both land (per task brief).

🤖 Generated with Claude Code

… to sid (B7.6)

Schema retirement #5 step 6: the precompute engine's per-bucket state used
to be keyed by `(agg_id, group_key)`. The grouping label values already
fold into the sid via `SeriesIdResolver`'s `(metric, attrs_fingerprint,
agg_kind_canonical)` identity contract, so the tuple collapses to a single
u64 — `sid` is now the bucket key throughout the ingest → router → worker
path.

Scope:
- `WorkerMessage::GroupSamples` / `AccumulatorInput` carry `sid: u64,
  policy_fp: PolicyFingerprint, group_key: String` instead of `agg_id +
  group_key`. `policy_fp` is the source-config handle the worker uses to
  look up `AggregationConfig` from the hot-reload snapshot; `group_key`
  still travels for emit-time `KeyByLabelValues` rendering. This is a
  BREAKING change to `WorkerMessage`, but the enum is private to the
  data_plane crate.
- `SeriesRouter::route_group_batch` hashes by `sid` alone
  (`worker_for_sid`). Same sid always lands on the same worker; bucket
  state stays single-owner.
- `Worker::group_states: HashMap<u64, GroupState>` (was
  `HashMap<(u64, String), GroupState>`). `GroupState` gains `policy_fp`
  and `group_key` fields so `evict_orphaned_groups` can check policy
  liveness and the emit path can render labels without re-keying the
  bucket.
- `process_group_samples` / `process_accumulator_input` take
  `(sid, policy_fp, group_key, ...)`. All in-crate call sites updated.
- OTLP ingest helper `resolve_bucket_sid_for_agg_config(state, config,
  point_labels) → (sid, policy_fp)`: derives the bucket sid for one
  `(config, DP)` pair by resolving against `(metric, grouping-label-
  values, ExactAgg-of-config)`. Used by all three OTLP dispatch paths
  (raw points, opaque SketchEnvelope, modified-OTLP first-class
  sketches). Crucially, "attrs" for sid purposes is the
  GROUPING-LABEL projection of wire labels — not the full label set —
  so distinct `(rack, node, pod)` tuples under a
  `grouping_labels=[zone]` policy still roll up into one bucket per
  zone (the GROUP-BY semantic).
- Regression test added: `drivers::ingest::otel::sid_bucketing_tests::
  raw_otlp_buckets_by_sid_with_distinct_group_keys` drives
  `route_otlp_to_precompute` end-to-end with two `zone` values × two
  DPs each, asserts exactly two `GroupSamples` are emitted with
  distinct non-zero sids that round-trip through
  `SeriesIdResolver::lookup`. The test docstring documents a
  pre-existing `format_series_key`/`parse_labels_from_series_key`
  inconsistency that makes `extract_group_key_for` return "" for OTLP
  inputs; B7.6 bucketing is unaffected because it reads
  `point.labels` directly (HashMap lookup), not the joined
  series_key.

Files touched (3):
- `data_plane/src/precompute_engine/series_router.rs` — message shape +
  routing hash + test rename.
- `data_plane/src/precompute_engine/worker.rs` — `GroupState` /
  `Worker.group_states` retyped, `get_or_create_group_state` /
  `process_group_samples` / `process_accumulator_input` /
  `evict_orphaned_groups` / `flush_all` reworked, 30+ test call sites
  updated to pass `(sid, PolicyFingerprint, group_key)`.
- `data_plane/src/drivers/ingest/otel.rs` — three OTLP dispatch paths
  switched to sid-bucketing via new `resolve_bucket_sid_for_agg_config`
  helper; added `sid_bucketing_tests` mod with the regression test.

Test plan:
- `cargo build -p data_plane` — clean.
- `cargo test -p data_plane --lib` — 713 passed / 2 ignored, no new
  failures vs. main.
- `cargo test -p data_plane` integration suite — same 2 pre-existing
  failures as origin/main (`controller_plan_to_query_full_roundtrip_
  ddsketch` / `_kll`); verified by re-running on origin/main HEAD.
  Unrelated to B7.6.

NOT in scope (left for B7.7):
- `output_sink.rs` already reads `output.policy_fp` (no `agg_id`); no
  changes needed there.
- `backfill/processor.rs` still uses `(agg_id, group_key)` internally —
  retired by B7.7.
- `AggregationConfig::aggregation_id()` accessor remains; retiring it
  is deferred until B7.6 + B7.7 both land (per task brief).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 6a668be into main May 18, 2026
zzylol added a commit that referenced this pull request May 18, 2026
…y roundtrip cleanly (#285)

`format_series_key` (in `drivers/ingest/otel.rs`) emitted unquoted
`key=value` pairs, but `parse_labels_from_series_key` (in
`precompute_engine/worker.rs`) required quoted `key="value"`. The
mismatch meant `IngestState::extract_group_key_for` returned `""`
for every OTLP wire-format input, and any other consumer that
roundtripped a freshly-formatted series key through the parser
got an empty label map back.

Discovery: PR #284 (B7.6 ingest sid rekey) noticed the bug while
implementing the bucketing rekey, side-stepped it by having the new
helper read `point.labels` directly via HashMap lookup, and left a
TODO-style note in the test docstring. Bucketing was unaffected
because B7.6 routes by sid; emit-time `KeyByLabelValues` content
elsewhere was silently broken.

Decision: option (B) — bend `format_series_key`. The quoted PromQL-
style `metric{k="v",...}` form is the canonical shape every other
producer / consumer in the data plane already uses:

  * `render_series_key` in
    `storage_engines/sketch_db/backfill/prometheus_reader.rs`
    emits quoted-with-escapes (matches the Prometheus wire format)
  * `RawSample.labels`' rustdoc documents the quoted form
  * `sample_matches` in
    `storage_engines/sketch_db/backfill/raw_sample_reader.rs` strips
    `"` from values when parsing — expects quoted
  * Every existing parser test passes the quoted form
  * Nothing persists `format_series_key`'s output to disk (it flows
    into in-memory `WorkerMessage::GroupSamples` payloads and debug
    log lines only)

`format_series_key` now escapes embedded `"`, `\`, `\n` per the
PromQL lexer rules (matching `render_series_key`'s
`escape_label_value`). The parser walks past `\<x>` escape pairs
when scanning for the closing quote so values containing embedded
`"` no longer terminate early. The returned `&str` is still the
raw (un-decoded) slice — a new `decode_label_value(&str) -> Cow<str>`
helper unescapes when needed. Returning the un-decoded slice keeps
the existing `HashMap<&str, &str>` API (and its `processor.rs`
caller, which is off-limits this PR for the B7.7 parallel agent)
working without change; most live callers compare against literal
config values that never contain escapable characters, so the
borrow is fine.

Regression coverage (new `series_key_roundtrip_tests` module in
`otel.rs` + new unit tests in `worker.rs`):
  * canonical PromQL quoted shape pinned
  * roundtrip with commas in value
  * roundtrip with equals in value
  * roundtrip with embedded `"` (exercises the escape pair scan +
    `decode_label_value`)
  * roundtrip with `\` in value
  * roundtrip with `\n` in value
  * roundtrip with all metacharacters in one value
  * empty-labels bare-braces case
  * `decode_label_value` borrows when no escapes, unescapes when present,
    passes unknown escapes through verbatim

All 725 `cargo test -p data_plane --lib` tests pass (2 pre-existing
ignored, unchanged). Updated the stale "this returns empty" note
in B7.6's `raw_otlp_buckets_by_sid_with_distinct_group_keys` test
docstring to point at this fix.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 18, 2026
…_id to sid (B7.7) (#286)

Third sub-step of schema-retirement #5 (issue #272), following B7.6's
ingest-side rekey. The backfill processor's per-window grouping is
now keyed by `sid: u64` instead of `group_key: String`, matching B7.6's
sid-bucketed worker state. The SketchStore exposes a new sid-direct
write path (`ingest_precompute_with_sid`) so callers that already hold
the bucket sid skip the resolver round-trip inside the mint-driven
`ingest_precompute_for_agg_config` wrapper.

## Sites rekeyed

- `data_plane/src/storage_engines/sketch_db/backfill/processor.rs`
  - `process_window` now groups raw samples into a
    `HashMap<u64, SidBucket>` (sid-keyed) instead of
    `HashMap<String, Vec<RawSample>>` (group_key-keyed).
  - New helper `resolve_backfill_bucket_sid` mirrors
    `resolve_bucket_sid_for_agg_config` from
    `drivers/ingest/otel.rs` so backfill and live ingest mint the
    SAME sid for the same `(metric, grouping-values, agg_kind)`
    tuple. This is the invariant that lets backfill writes land in
    the same store row live ingest already populated for
    `[created_at, ∞)`.
  - Per-bucket writes go through the new `ingest_precompute_with_sid`
    path; the mint-driven sibling is no longer called from this
    file.
  - Resolver-less fallback (legacy / registry-only test setups)
    keeps a stable per-`group_key` bucket id so accumulator builds
    still preserve sample ordering — but the write itself is
    skipped in that branch anyway (no resolver ⇒ no precompute
    write, matching pre-B7.7 behaviour).

- `data_plane/src/storage_engines/sketch_db/index/mod.rs`
  - New `pub fn ingest_precompute_with_sid(sid, agg_cfg, output,
    accumulator)` takes the bucket sid directly. The existing
    `ingest_precompute_for_agg_config` is refactored into a thin
    mint-driven wrapper that delegates to the new entry point —
    callers that don't yet hold the sid (the live `SketchStoreSink`)
    keep working unchanged.
  - Extracted `build_attrs_fp_and_label_map` shared by both methods
    so the mint-driven path (B7.6) and the sid-direct path (B7.7)
    stay byte-identical on the values they hand to the index.

## Tests added

- `process_window_buckets_by_sid_via_resolver` — drives
  `process_window` end-to-end with two distinct svc values × two
  samples each, asserts exactly two sids land in the SketchStore,
  both `classify()` as `Hit`, and registry provenance is one entry
  per window.
- `backfill_sid_matches_live_ingest_sid_for_same_grouping_values` —
  locks the live-vs-backfill sid namespace invariant: the sid the
  backfill helper computes for `(cfg, "latency{svc=a,zone=z0}")`
  must equal what the live ingest path's
  `resolve_bucket_sid_for_agg_config` mirror computes for the same
  `(metric, grouping-values, agg_kind)` tuple via the SAME
  resolver.

## Not in scope (deferred follow-ups)

- `output_sink.rs` production code already consumes
  `output.policy_fp` (PR #284's report: "no changes needed there").
  Its only `aggregation_id()` site is in a test that builds a
  `StreamingConfig` map keyed by policy_fp.as_u64() — the
  legitimate policy-registry use, not a bucket key.
- `worker.rs` / `series_router.rs` / `drivers/ingest/otel.rs` are
  B7.6's domain (already merged) — not touched.
- Remaining `aggregation_id()` accessor sites are all
  test-side `StreamingConfig` map-key uses (the map IS keyed by
  policy_fp.as_u64()) — those stay until the accessor itself is
  retired after #4 (re-enable ignored tests).

## Test plan

- `cargo build -p data_plane` — clean
- `cargo test -p data_plane --lib` — 715 passed / 2 ignored, no
  regressions vs origin/main
- `cargo test -p data_plane` integration suite — same 2 pre-existing
  failures `controller_plan_to_query_full_roundtrip_ddsketch` /
  `_kll` PR #284 confirmed are pre-existing
- Both new regression tests pass

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol deleted the b7.6-ingest-bucketing-sid branch July 17, 2026 20:05
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