Skip to content

feat(data_plane): implement SummaryExecutor for cumulative quantile/cardinality (Step C, first cut) - #411

Merged
zzylol merged 8 commits into
mainfrom
feat/data-plane-summary-executor
Jul 25, 2026
Merged

zzylol merged 8 commits into
mainfrom
feat/data-plane-summary-executor

Conversation

@zzylol

@zzylol zzylol commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Step C of the plan-shaped-serving migration (data_plane/docs/l4node-plan-executor-design.md, #409). Stacked on #410 (rev pin bump) since asap_sketch::exec::SummaryExecutor doesn't exist before that.

Scoped to quantile/cardinality queries (DDSketch/Kll/Hll) and the Frequency family's bare total (CMS/CountSketch/CMS-with-heap/CountSketch-with-heap — count/sum with no specific item key), both cumulative (instant) and per-window (matrix/range) — see the module doc in summary_executor.rs for exactly what's in/out of scope. Explicitly deferred, erroring rather than silently mishandled: SketchQuery::TopK (its answer shape — K items per timestamp — doesn't fit this executor's Value = Vec<(i64, f64)> at all; needs a Value type redesign, a separate decision) and PointCount with a named item key (the filter value isn't carried by SketchQuery or available in readout's signature).

Seven commits:

  1. refactor(delta_apply): generalize cumulative_hll_state to all sketch families — the DD/KLL/HLL cross-sid merge primitive.

  2. feat(data_plane): implement SummaryExecutor for cumulative quantile/cardinalityQueryExecutionContext implements the trait; exact (SummaryKind, SummaryParams) matching; lazy fetch_state/merge_states, real merge in readout.

  3. feat(data_plane): per-window (matrix/range-query) readout for SummaryExecutorper_window_rolling_states, cross-sid per-window merge.

  4. fix(data_plane): address PR review feedback on SummaryExecutor — removed the now-redundant cumulative_hll_state wrapper, rewrote comments to describe current design instead of history, fixed a real inefficiency (double query_range fetch per candidate — SidHandle now carries the already-fetched series via Rc).

  5. feat(data_plane): unify Frequency family into SummaryState — CMS/CountSketch/CMS-with-heap/CountSketch-with-heap folded into the same SummaryState/merge_same_family pipeline DD/KLL/HLL already use (not a separate parallel implementation — see the PR comment on this commit for why the original separate-file approach was wrong).

  6. fix(data_plane): wire real asap_sketchlib::CountSketchWithHeap into SummaryStateCmsWithHeap and CountSketchWithHeap were still sharing one underlying wire type (CountMinSketchWithHeap, CMS's min-over-rows estimator) because asap_sketchlib had no separate CountSketchWithHeap (median-of-signed-rows estimator) type. Rather than work around that gap in data_plane, added the real type upstream (ProjectASAP/asap_sketchlib#78, now merged) and wired it in here: new decode_cs_with_heap_from_msgpack{,_delta} decoders, SummaryState::CountSketchWithHeap now holds the genuine type. The two variants holding distinct Rust types means a cross-family merge is now a type-level impossibility, not just a runtime check.

  7. fix(sketch_reducer): route CountSketchWithHeap through its own estimator — the same conflation bug, found separately: sketch_reducer.rs's legacy decode_frequency_estimate (a different, still-live code path from SummaryState/delta_apply.rs) always decoded heap-bearing sids through CountMinSketchWithHeap and rebuilt a plain CountMinSketch regardless of the sid's real kind — silently wrong for CountSketchWithHeap's per-key topk/FrequencyEstimate queries. Split the collapsed match arms in decode_frequency_total, decode_frequency_estimate, and the FrequencyTopk per-frame heap-decode loop to dispatch on the sid's actual kind.

Also adds asap-sketch/asap-ir as direct data_plane dependencies (pin-matched to control_plane's — required even though control_plane::asap_tier_implement already returns Vec<Rc<L4Node>>, since implementing a trait on / matching a type's variants requires importing its defining crate directly).

Leaving this unmerged for review, per the same reasoning as #410/#161: this changes the live query-serving path's dependency surface and introduces a new, not-yet-wired-in code path — worth a human look before anything merges, especially the scope boundary (what's covered vs. deferred) and the find_candidates/lazy-state design.

Test plan

  • cargo test -p data_plane --lib summary_executor — 12 tests, all passing. The ones that matter most:
    • two_sids_same_group_actually_merge_not_just_first / two_cms_sids_same_group_totals_actually_merge — proves real cross-sid merge for both the Sketch and Frequency families.
    • two_sids_different_groups_produce_two_series_not_one_merged_blob — the feat(engine): read precomputes from SketchIndex when attached #159 fix, exercised end-to-end.
    • per_window_matrix_produces_multiple_points_for_one_sid / per_window_matrix_merges_across_sids_per_window — matrix output correctness.
    • topk_query_is_explicitly_unsupported_not_silently_wrong — the scope boundary errors instead of returning wrong data.
  • cargo test -p data_plane --lib delta_apply — 11 tests, all passing, including cms_with_heap_and_count_sketch_with_heap_are_not_the_same_family (now backed by genuinely distinct types) and count_sketch_with_heap_full_and_delta_decode_via_new_asap_sketchlib_type (new — proves both the FULL and DELTA-HEAP decode paths reconstruct the same matrix/estimate() as the in-memory sketch they were encoded from).
  • cargo test -p data_plane --lib frequency_heap_tests — new decode_frequency_estimate_uses_each_kinds_own_estimator, built via real update() calls on both kinds and asserting decode_frequency_estimate reproduces each kind's own in-memory estimate() truth.
  • cargo test -p data_plane sketch_db:: — 294 passed, confirms the evaluate_cardinality_global refactor is behaviorally identical.
  • cargo test -p data_plane --lib — full lib suite: 908 passed, 2 ignored. Only pre-existing, unrelated failures elsewhere in the workspace, verified identical on the parent commit before this branch's changes existed.
  • cargo build --workspace — clean, against the real (now-merged) asap_sketchlib main — no local [patch] workaround needed.
  • cargo clippy -p data_plane --lib -- -D warnings — zero warnings in the touched files.

🤖 Generated with Claude Code

/// reducer merges the returned sketches across series (register-wise max)
/// before estimating, so the answer is the distinct UNION cardinality, not
/// the sum of per-series cardinalities.
pub fn cumulative_hll_state(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to keep it separately if cumulative rolling state func covers it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed — evaluate_cardinality_global now calls cumulative_rolling_state directly and reads out via RollingState::cardinality() instead. Confirmed via the existing 294-test sketch_db suite passing unchanged.

Comment thread data_plane/src/storage_engines/sketch_db/query/delta_apply.rs
zzylol added a commit that referenced this pull request Jul 25, 2026
…Executor

Closes the "per-window/matrix output" gap flagged as follow-up in the
previous commit -- SummaryExecutor now covers both cumulative (instant)
and per-window (matrix/range) quantile/cardinality queries.

delta_apply.rs: adds per_window_rolling_states, generalizing
per_window_evaluate the same way cumulative_rolling_state generalized
cumulative_hll_state -- returns each window's reconstructed RollingState
instead of an already-evaluated scalar, so a caller can merge same-window
states across several sids before evaluating. per_window_evaluate now
delegates to it (behaviorally identical: same decode_full/apply_delta_bytes
walk, verified by the existing 294-test sketch_db suite passing unchanged).

summary_executor.rs: readout now dispatches on is_cumulative to
readout_cumulative (existing) or the new readout_per_window, which
reconstructs each of a group's sids' own per-window states, then unions
by window_end and merges same-window states across sids (mirroring
SummaryMerge's "fold whatever's present" semantics from ASAPController#161
-- a sid missing a particular window just doesn't contribute to it, the
window isn't dropped). Drops carry-in base windows ending before t0,
matching sketch_reducer.rs's evaluate_core's identical existing filter.

## Test plan
- [x] cargo test -p data_plane --lib summary_executor -- 9 tests (6
      existing + 3 new), all passing:
  - per_window_matrix_produces_multiple_points_for_one_sid -- basic
    multi-window correctness.
  - per_window_matrix_merges_across_sids_per_window -- the cross-sid
    analog of the cumulative merge test: two sids sharing one window_end
    produce one merged point reflecting both, not either alone.
  - per_window_matrix_drops_carry_in_base_before_t0.
- [x] cargo test -p data_plane -- full suite: 902 passed in the main lib
      target (was 896 before this commit's 6 new tests... note: 3 net
      new here, 3 from the prior commit), same pre-existing unrelated
      e2e failures as already documented on PR #411.
- [x] cargo build --workspace -- clean.
- [x] cargo clippy -p data_plane --lib -- -D warnings -- zero warnings
      in the touched files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol

zzylol commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Added the range-query (matrix) follow-up in a second commit — SummaryExecutor now covers both cumulative (instant) and per-window (matrix/range) quantile/cardinality queries, not just cumulative.

  • delta_apply.rs: new per_window_rolling_states, generalizing per_window_evaluate the same way cumulative_rolling_state generalized cumulative_hll_state — returns each window's reconstructed state instead of an already-evaluated scalar, so callers can merge same-window states across sids before evaluating. per_window_evaluate now delegates to it (verified behaviorally identical: same 294-test sketch_db suite passes unchanged).
  • summary_executor.rs: readout dispatches on is_cumulative to readout_cumulative (existing) or the new readout_per_window, which unions windows across a group's sids and merges same-window states (mirrors SummaryMerge's "fold whatever's present" semantics from chore(precompute_engine): delete unused RawPassthroughSink #161/ASAPController#159 — a sid missing a window doesn't drop the window, just doesn't contribute to it). Drops carry-in base windows ending before t0, matching evaluate_core's existing identical filter on the legacy path.

3 new tests (multi-window correctness, cross-sid per-window merge, carry-in-base filtering) — all passing, plus the existing 6 unaffected. Same pre-existing unrelated e2e failures as before, still unrelated (verified).

Remaining scoped-out items, unchanged from before: Frequency family (TopK/CMS) and ExactAgg intents (need caller-side handling, not readout).

🤖 Generated with Claude Code

@@ -0,0 +1,1041 @@
//! `data_plane`'s `asap_sketch::exec::SummaryExecutor` implementation —
//! Step C of the plan-shaped-serving migration

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make the code comments not showing step X, phase Y etc, and not talking about the detailed wrong case before, but just why currently we have this design and how it is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrote the module doc and every inline comment to describe the current design and its rationale, not the sequence of steps/gaps/issue numbers that produced it (no more "Step C", "#409", "ASAPController#161", "gap #409 flagged", etc.).

@zzylol

zzylol commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Can you check any inefficiency in SummaryExecutor implementation, e.g., memory copy, not needed deserialization/serialization, any potential caching, any redundant indexing/inefficient indexing? Any extra memory usage, computation overhead?

zzylol added a commit that referenced this pull request Jul 25, 2026
Three fixes, addressing review comments on PR #411:

1. Removes the now-redundant cumulative_hll_state wrapper -- its one
   caller (evaluate_cardinality_global) now calls cumulative_rolling_state
   directly and reads out via RollingState::cardinality() instead of
   HllSketch::estimate(), since cumulative_rolling_state already covers
   the HLL case. Verified via the existing 294-test sketch_db suite
   passing unchanged.

2. Rewrites summary_executor.rs's comments to describe the current
   design and its rationale, not the sequence of steps/issues that
   produced it.

3. Fixes a real inefficiency: find_candidates was calling query_range
   (which clones every in-range sample's bytes to build its owned
   return value) to read a candidate sid's label values for the group
   key, and then fetch_state/readout called query_range AGAIN on the
   same (sid, t0, t1) to get the actual sample data for decoding --
   double the clone/decode work per candidate, and a narrow window
   where the two calls could observe different data under concurrent
   writes. SidHandle now carries the already-fetched series (Rc, so
   further clones are just a refcount bump) from find_candidates through
   to fetch_state/merge_states/readout, so query_range is called exactly
   once per candidate. Also folds delta_kind onto the handle at the same
   time (find_candidates already looks up the sid's SketchConfig for the
   exact-match check), removing a second with_instance lookup
   fetch_state used to make on its own.

## Test plan
- [x] cargo test -p data_plane --lib summary_executor -- 9 tests, all
      passing (caught and fixed two real bugs introduced while doing
      this refactor before it ever reached this point: a per-window
      carry-in-base filter that lost access to t0_ms when readout moved
      to a free function, and a latest-window-end fallback that would
      have always reported t1_ms instead of the real latest window).
- [x] cargo test -p data_plane sketch_db:: -- 294 passed, confirms the
      evaluate_cardinality_global change is behaviorally identical.
- [x] cargo test -p data_plane -- full suite: 902 passed (main lib
      target), same pre-existing unrelated failures as already
      documented on PR #411.
- [x] cargo clippy -p data_plane --lib -- -D warnings -- zero warnings
      in the touched files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol

zzylol commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Efficiency review, and one real fix landed (pushed):

Fixed — double fetch + double decode. find_candidates called query_range(sid, t0, t1) per candidate just to read label values for the group key (that's the only place per-sid label values live). readout then called query_range again on the same (sid, t0, t1) to get the actual sample data. query_range returns an owned Vec<SketchTimeSeries> — building it clones every in-range sample's bytes — so this was doubling the clone/decode work per candidate sid, plus leaving a narrow window where the two calls could observe different data under a concurrent write between them. Fixed: SidHandle (the trait's Handle type) now carries the already-fetched series as Rc<SketchTimeSeries> from find_candidates through fetch_state/merge_states/readout, so query_range is called exactly once per candidate. Also folds delta_kind onto the handle at the same time (find_candidates already looks up the sid's SketchConfig for the exact-param-match check), removing a second with_instance lookup fetch_state used to make on its own.

Considered, not changed:

  • readout's per-entry Vec<(i64, &SketchSampleState)> rebuild (flat_map().collect()) — a real allocation, but it's the exact same pattern sketch_reducer.rs's evaluate_core/evaluate_exact_agg already use throughout the existing codebase, not something new I introduced. Changing it to avoid the allocation would mean changing delta_apply.rs's function signatures to take an iterator instead of a slice — a bigger, more invasive change than this PR's scope.
  • GroupKey: BTreeMap<String, String> label cloning per candidate — required by the trait's GroupKey: Clone bound and matches the BTreeMap<String,String> label-map convention already used throughout the reducer; no cheaper representation available without a broader label-interning change (which is what design-backend-plan-wire-format.md's RoutingIndex Tier-2 design already proposes for a different reason — group lookup, not just this).
  • Rc clone cost elsewhere: fetch_state clones one SidHandle (an Rc bump + a Copy enum) per handle, merge_states just extends Vecs — no unnecessary deep copies anywhere in the group-assembly path.

Verified no regression: cargo test -p data_plane --lib summary_executor (9/9) and cargo test -p data_plane sketch_db:: (294/294) both still pass after the fix.

🤖 Generated with Claude Code

zzylol added a commit that referenced this pull request Jul 25, 2026
The design table and open questions described the target shape before
implementation started; several turned out to differ once real code got
written. Updates:

- Status note: SummaryExecutor is implemented and tested for
  quantile/cardinality (DDSketch/Kll/Hll), both cumulative and
  per-window, in #411 (unmerged). Frequency family (CMS/CountSketch) in
  progress; ExactAgg readout and wiring execute() into the live serving
  path still not started.
- Table: Handle is SidHandle (sid + already-fetched series + decode
  params), not a bare u64 -- find_candidates needs the series anyway
  for the group key, so it's threaded through rather than re-fetched.
  merge_states/fetch_state are deliberately lazy; the real merge math
  lives in readout, which is the only place that knows
  cumulative-vs-per-window mode.
- Open question 1 (tree source): resolved as both -- the
  control_plane seam for planning-time tree construction, plus a
  direct asap-sketch/asap-ir dependency for everything serving-time
  (implementing the trait / matching its types requires importing the
  defining crate directly, not just consuming a function that returns
  those types).
- Open question 2: partially resolved -- KLL/DDSketch/HLL merge done
  (via a generalized existing HLL-only primitive, not built from
  scratch), CMS/CountSketch still open. Partial-group-coverage question
  resolved (fold whatever's present). Accuracy-math and
  resize/downsample questions remain genuinely open.
- Questions 3-4 unchanged -- neither addressed yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
zzylol added a commit that referenced this pull request Jul 25, 2026
… follow-up)

Started as a separate frequency_apply.rs (CMS/CountSketch/heap-bearing
decode+merge), then unified into delta_apply.rs after reconsidering: the
Frequency family's "decode each frame independently, then merge" is the
same fold operation delta_apply.rs's DD/KLL/HLL families already use for
every delta case except two genuine sparse-in-place special cases (DD's
bucket-index proto delta, HLL's register proto delta) -- both already
decode via decode_full-and-merge for their other encodings. So it's one
merge_same_family/cumulative_rolling_state/per_window_rolling_states
pipeline for all six sketch kinds, not two parallel ones.

- RollingState renamed to SummaryState (no longer just the "rolling"
  DD/KLL/HLL families).
- DeltaSketchKind gains Cms/CountSketch/Heap variants (Heap covers both
  CmsWithHeap and CountSketchWithHeap -- they already share one wire
  representation and read out identically in the existing reducer code).
- decode_full/apply_delta_bytes/merge_same_family extended; new
  total()/topk_items() readout accessors.
- summary_executor.rs: dropped the CandidateKind wrapper enum this no
  longer needs -- SidHandle/GroupState carry one DeltaSketchKind
  directly. summary_params_match/to_delta_kind extended for
  Cms/CmsWithHeap/CountSketch/CountSketchWithHeap (width=cols/depth=rows,
  matching the existing wire convention; SketchConfig has no heap_size
  field at all, since heap-bearing kinds reuse their heap-less base's
  config shape for sid identity -- confirmed via
  drivers/ingest/otel.rs's base_sketch_kind_handle).

Also refactored evaluate_cardinality_global (sketch_reducer.rs) to use
SummaryState directly, dropping the redundant cumulative_hll_state
wrapper this obsoletes.

Scope actually covered: quantile/cardinality (unchanged) plus the
Frequency family's BARE total (SketchQuery::PointCount{key:
ColumnRef::SampleValue}) -- no specific item key, both cumulative and
per-window. Explicitly NOT covered, and erroring rather than silently
wrong:
- SketchQuery::TopK -- its answer (K items per timestamp) doesn't fit
  this executor's Value = Vec<(i64, f64)> shape at all; forcing it in
  would silently drop data. Needs a Value type redesign, a separate
  decision.
- PointCount with a named item key -- the value to filter by isn't
  carried by SketchQuery or available in readout's signature at all.

## Test plan
- [x] cargo test -p data_plane --lib summary_executor -- 12 tests (9
      existing + 3 new), all passing:
  - single_cms_sid_total_readout / two_cms_sids_same_group_totals_actually_merge
    -- same two properties proven for DD/KLL earlier (real readout, real
    cross-sid merge via matrix addition), now for CMS.
  - topk_query_is_explicitly_unsupported_not_silently_wrong -- proves
    the scope boundary errors instead of returning a wrong/truncated
    answer.
- [x] cargo test -p data_plane sketch_db:: -- 294 passed, confirms
      evaluate_cardinality_global's refactor is behaviorally identical.
- [x] cargo test -p data_plane -- full suite: 905 passed (main lib
      target), same pre-existing unrelated failures already documented
      on PR #411.
- [x] cargo build --workspace / cargo clippy -p data_plane --lib -- -D
      warnings -- clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol

zzylol commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Frequency family (CMS/CountSketch) follow-up landed, in two parts:

Part 1 (superseded by part 2, kept as commit history): started as a separate `frequency_apply.rs`, justified by "no rolling reconstruction needed for this family." That framing was wrong — re-examined the actual delta-decode implementations and found the Frequency family's "decode independently, then merge" is the same fold operation `delta_apply.rs` already uses for every DD/KLL/HLL delta case except two genuine sparse-in-place special cases (DD's bucket-index proto delta, HLL's register proto delta) — both of which also decode-and-merge for their other encodings.

Part 2: unified. `RollingState` → `SummaryState`, extended with `Cms`/`CountSketch`/`Heap` variants (Heap covers both `CmsWithHeap` and `CountSketchWithHeap` — they already share one wire representation and read out identically in the existing reducer). One `merge_same_family`/`cumulative_rolling_state`/`per_window_rolling_states` pipeline for all six sketch kinds now, not two parallel ones. `frequency_apply.rs` deleted. `summary_executor.rs`'s `CandidateKind` wrapper enum also dropped — no longer needed once there's one unified kind.

Real finding worth flagging explicitly: `SketchQuery::TopK`'s answer (K items per timestamp) doesn't fit this executor's `Value = Vec<(i64, f64)>` shape at all — forcing it in (e.g. returning only the top item) would silently drop data rather than error. Scoped out, with a test (`topk_query_is_explicitly_unsupported_not_silently_wrong`) proving it errors instead of returning something wrong. Needs a `Value` type redesign — a separate decision, not bundled into this PR.

Also refactored `evaluate_cardinality_global` to use `SummaryState` directly and dropped the now-redundant `cumulative_hll_state` wrapper this obsoletes (following up on the earlier review comment about it).

Actual scope covered now: quantile/cardinality (unchanged) + the Frequency family's bare total (no specific item key), both cumulative and per-window. PointCount with a named item key and TopK both explicitly unsupported, both with a documented reason in the module doc.

Test plan

  • 12 tests in summary_executor (9 existing + 3 new), all passing — CMS total readout, CMS cross-sid merge (matrix addition, same property already proven for DD/KLL), and the TopK-errors-don't-silently-return-wrong-data test.
  • sketch_db:: suite (294 tests) confirms the evaluate_cardinality_global refactor is behaviorally identical.
  • Full suite: 905 passed, same pre-existing unrelated failures already documented on this PR.
  • clippy -p data_plane --lib -- -D warnings: clean.

🤖 Generated with Claude Code

zzylol added a commit that referenced this pull request Jul 25, 2026
…ulative/per_window_summary_state

Addresses two new review comments on PR #411.

1. SummaryState::Heap collapsed CmsWithHeap and CountSketchWithHeap into
   one shared variant, reasoning that they share a wire representation
   (asap_sketchlib has no separate CountSketchWithHeap type) and today's
   reducer reads both out identically. That reasoning missed a real bug:
   a CMS-substrate heap and a CountSketch-substrate heap are different
   sketch algorithms that merely happen to share a storage shape --
   merge_same_family's single Heap-Heap arm would have silently allowed
   merging one into the other (mathematically invalid, but type-checks
   fine since both wrap CountMinSketchWithHeap). Split into two distinct
   variants (CmsWithHeap/CountSketchWithHeap), both still backed by
   CountMinSketchWithHeap since that's the only type available, but now
   merge_same_family's per-variant match rejects the cross-family case
   the same way it already rejects e.g. merging a Cms into a Kll.
2. Renamed cumulative_rolling_state -> cumulative_summary_state and
   per_window_rolling_states -> per_window_summary_states, matching the
   RollingState -> SummaryState rename from the prior commit (the
   function names were left stale).

## Test plan
- [x] New test: cms_with_heap_and_count_sketch_with_heap_are_not_the_same_family
      -- proves the fix: merge_same_family now rejects the cross-family
      case with a family-mismatch error instead of silently succeeding.
- [x] cargo test -p data_plane --lib summary_executor -- 12/12 passing,
      unaffected.
- [x] cargo test -p data_plane --lib delta_apply -- 10/10 passing (9
      existing + 1 new).
- [x] cargo test -p data_plane -- full suite: 906 passed (main lib
      target), same pre-existing unrelated failures already documented
      on PR #411.
- [x] cargo clippy -p data_plane --lib -- -D warnings -- clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol

zzylol commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both new comments (couldn't reply inline — the pending-review comments can't take a threaded reply while that review is still open, so posting here instead):

"there should be a CountSketchWithHeap here" (delta_apply.rs) — confirmed asap_sketchlib has no separate CountSketchWithHeap type at all (only CountMinSketchWithHeap), so I'd originally collapsed both CmsWithHeap and CountSketchWithHeap into one shared Heap variant. That was a real bug, not just a naming choice: a CMS-substrate heap and a CountSketch-substrate heap are different sketch algorithms that merely share a storage shape — merge_same_family's single Heap-Heap arm would have silently allowed merging one into the other (mathematically invalid, but type-checks fine since both wrap CountMinSketchWithHeap). Fixed: split into two distinct enum variants (CmsWithHeap/CountSketchWithHeap), still both backed by CountMinSketchWithHeap (no other type exists), but now merge_same_family's per-variant match rejects the cross-family case the same way it already rejects e.g. merging a Cms into a Kll. Added cms_with_heap_and_count_sketch_with_heap_are_not_the_same_family proving the fix.

"cumulative_rolling_state should be renamed to cumulative_summary_state to match SummaryState" — done, and per_window_rolling_statesper_window_summary_states too (same staleness, missed in the rename commit).

Pushed. 906/906 passing (main lib target), 12/12 summary_executor, 10/10 delta_apply (9 existing + the new regression test), clippy -D warnings clean.

🤖 Generated with Claude Code

Comment thread data_plane/src/storage_engines/sketch_db/query/delta_apply.rs
t1_ms: u64,
) -> Result<ASAPTierResult, ASAPTierError> {
use super::delta_apply::cumulative_hll_state;
use super::delta_apply::{cumulative_rolling_state, DeltaSketchKind, SummaryState};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cumulative_rolling_state should be renamed to cumulative_summary_state to match SummaryState

@zzylol

zzylol commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up on the "there should be a CountSketchWithHeap here" comment: rather than keep working around the gap in data_plane, added the real type to asap_sketchlib (ProjectASAP/asap_sketchlib#78, merged) and wired it in here in 61f3425. SummaryState::CountSketchWithHeap now holds the genuine asap_sketchlib::CountSketchWithHeap (median-of-signed-rows estimator) instead of aliasing the CMS-family CountMinSketchWithHeap (min-over-rows estimator). New decoders in decoders.rs, updated decode_full/apply_delta_bytes/total/topk_items in delta_apply.rs. The two SummaryState variants now hold genuinely different Rust types, so a cross-family merge is a compile-time impossibility, not just a runtime check — see the updated cms_with_heap_and_count_sketch_with_heap_are_not_the_same_family test plus the new count_sketch_with_heap_full_and_delta_decode_via_new_asap_sketchlib_type regression test.

@zzylol
zzylol changed the base branch from chore/bump-asapcontroller-rev-summaryexecutor to main July 25, 2026 17:44
zzylol and others added 8 commits July 25, 2026 11:44
…families

Adds RollingState::merge_same_family and a generic
cumulative_rolling_state, following the exact decode/merge/apply_delta
pattern cumulative_hll_state already used for the HLL-only global
cardinality rollup -- generalized to DD/KLL too. cumulative_hll_state
itself now delegates to the generic function (behaviorally identical:
same decode_full/merge/apply_delta_bytes calls, just reachable for any
RollingState family instead of hardcoded to Hll).

This is the cross-sid merge building block SummaryExecutor::merge_states
needs (Step C, #409): reconstruct each candidate sid's own RollingState
over the query range via cumulative_rolling_state, then fold them
together via merge_same_family before reading out one cross-sid answer
-- the same real bug evaluate_cardinality_global already fixed for the
HLL-global special case (issue: every other grouped sketch case still
emits duplicate un-merged series today), generalized so it isn't
HLL-only anymore.

Also picks up asap_sketchlib's updated Cargo.lock entry (serde_bytes
dependency, from the msgpack wire format work already on asap_sketchlib
main) via the local path-patch sibling checkout.

Verified: cargo test -p data_plane (sketch_db module) -- 294 passed, 0
failed, including the existing HLL global-cardinality tests this
refactor touches indirectly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ardinality

Step C of the plan-shaped-serving migration
(data_plane/docs/l4node-plan-executor-design.md), scoped to cumulative
(instant) quantile/cardinality queries over the DDSketch/Kll/Hll
families -- the ones `RollingState` (delta_apply.rs, generalized in the
prior commit) covers. See the module doc for exactly what's in and out
of scope for this first cut (per-window/matrix output and the Frequency
family are explicitly deferred, not silently mishandled).

`QueryExecutionContext` implements `asap_sketch::exec::SummaryExecutor`
-- constructed fresh per incoming query (never shared, never mutated),
carrying `t0_ms`/`t1_ms`/`is_cumulative` as plain fields. The trait
itself has no time-range parameter and `ASAPQueryEngine` is called
concurrently, so this is the safe alternative to threading the range
through shared mutable state on the engine.

- `find_candidates`: walks the `SummaryAgg`'s child subtree down to a
  `Scan { source: Source::TimeSeries { metric }, .. }` to recover the
  metric (mirrors `control_plane::asap_tier_implement::collect_aggregate_roots`'s
  recursion style over the same `QueryExpr` type), resolves `by`
  `ColumnId`s to names against the child's `L4Schema`, and filters
  candidate sids by EXACT `(SummaryKind, SummaryParams)` match (not the
  family-level `Capability::is_satisfied_by` the legacy analyzer path
  uses) -- required so `SummaryMerge`'s precondition holds by
  construction.
- `fetch_state`/`merge_states` are deliberately lazy: they just
  accumulate a group's sid list. The real cross-sid merge (the actual
  fix for the "duplicate un-merged series" gap #409 flagged) happens in
  `readout`, via `cumulative_rolling_state` + `RollingState::merge_same_family`
  -- reusing the exact primitives the HLL-only global-cardinality rollup
  already proved correct, generalized to DD/KLL too.
- `logical`: errors (matches today's CapabilityMiss-and-fail-over-to-archive
  contract).

Adds `asap-sketch`/`asap-ir` as direct `data_plane` dependencies
(pin-matched to `control_plane`'s, same rule as the existing
`crates/asap_types/Cargo.toml` pin comment) -- required even though
`control_plane::asap_tier_implement` already returns `Vec<Rc<L4Node>>`,
because implementing a trait on / matching variants of a type requires
importing its defining crate directly.

## Test plan
- [x] `cargo test -p data_plane --lib summary_executor` -- 6 new tests,
      all passing:
  - `single_kll_sid_quantile_readout` -- basic correctness.
  - `two_sids_same_group_actually_merge_not_just_first` -- proves real
    merge: median of two sids' disjoint value ranges lands between
    both, not at either one alone (would fail if merge silently
    dropped a sid).
  - `two_sids_different_groups_produce_two_series_not_one_merged_blob`
    -- the ASAPController#159 fix, exercised end-to-end through
    `asap_sketch::exec::execute()`: two zones produce two independent
    series with correct per-zone values, not one merged blob.
  - `hll_cardinality_readout`, `no_matching_sid_is_no_candidates`,
    `mismatched_params_does_not_match` (exact-param-match, not
    family-only).
- [x] `cargo test -p data_plane` -- full suite, only pre-existing,
      unrelated failures (verified identical on the parent commit
      before this file existed): the same
      `optimizer::rules::tests::invalid_sketch_type_override_falls_back_to_default`
      failure from the rev-bump commits, plus 4 pre-existing
      `e2e_controller_plans_and_backend_serves` CMS/CountSketch
      failures (verified identical with `git stash` against the parent
      commit).
- [x] `cargo build --workspace` -- clean.
- [x] `cargo clippy -p data_plane --lib -- -D warnings` -- zero
      warnings in this file (pre-existing warnings elsewhere in the
      crate untouched).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…Executor

Closes the "per-window/matrix output" gap flagged as follow-up in the
previous commit -- SummaryExecutor now covers both cumulative (instant)
and per-window (matrix/range) quantile/cardinality queries.

delta_apply.rs: adds per_window_rolling_states, generalizing
per_window_evaluate the same way cumulative_rolling_state generalized
cumulative_hll_state -- returns each window's reconstructed RollingState
instead of an already-evaluated scalar, so a caller can merge same-window
states across several sids before evaluating. per_window_evaluate now
delegates to it (behaviorally identical: same decode_full/apply_delta_bytes
walk, verified by the existing 294-test sketch_db suite passing unchanged).

summary_executor.rs: readout now dispatches on is_cumulative to
readout_cumulative (existing) or the new readout_per_window, which
reconstructs each of a group's sids' own per-window states, then unions
by window_end and merges same-window states across sids (mirroring
SummaryMerge's "fold whatever's present" semantics from ASAPController#161
-- a sid missing a particular window just doesn't contribute to it, the
window isn't dropped). Drops carry-in base windows ending before t0,
matching sketch_reducer.rs's evaluate_core's identical existing filter.

## Test plan
- [x] cargo test -p data_plane --lib summary_executor -- 9 tests (6
      existing + 3 new), all passing:
  - per_window_matrix_produces_multiple_points_for_one_sid -- basic
    multi-window correctness.
  - per_window_matrix_merges_across_sids_per_window -- the cross-sid
    analog of the cumulative merge test: two sids sharing one window_end
    produce one merged point reflecting both, not either alone.
  - per_window_matrix_drops_carry_in_base_before_t0.
- [x] cargo test -p data_plane -- full suite: 902 passed in the main lib
      target (was 896 before this commit's 6 new tests... note: 3 net
      new here, 3 from the prior commit), same pre-existing unrelated
      e2e failures as already documented on PR #411.
- [x] cargo build --workspace -- clean.
- [x] cargo clippy -p data_plane --lib -- -D warnings -- zero warnings
      in the touched files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three fixes, addressing review comments on PR #411:

1. Removes the now-redundant cumulative_hll_state wrapper -- its one
   caller (evaluate_cardinality_global) now calls cumulative_rolling_state
   directly and reads out via RollingState::cardinality() instead of
   HllSketch::estimate(), since cumulative_rolling_state already covers
   the HLL case. Verified via the existing 294-test sketch_db suite
   passing unchanged.

2. Rewrites summary_executor.rs's comments to describe the current
   design and its rationale, not the sequence of steps/issues that
   produced it.

3. Fixes a real inefficiency: find_candidates was calling query_range
   (which clones every in-range sample's bytes to build its owned
   return value) to read a candidate sid's label values for the group
   key, and then fetch_state/readout called query_range AGAIN on the
   same (sid, t0, t1) to get the actual sample data for decoding --
   double the clone/decode work per candidate, and a narrow window
   where the two calls could observe different data under concurrent
   writes. SidHandle now carries the already-fetched series (Rc, so
   further clones are just a refcount bump) from find_candidates through
   to fetch_state/merge_states/readout, so query_range is called exactly
   once per candidate. Also folds delta_kind onto the handle at the same
   time (find_candidates already looks up the sid's SketchConfig for the
   exact-match check), removing a second with_instance lookup
   fetch_state used to make on its own.

## Test plan
- [x] cargo test -p data_plane --lib summary_executor -- 9 tests, all
      passing (caught and fixed two real bugs introduced while doing
      this refactor before it ever reached this point: a per-window
      carry-in-base filter that lost access to t0_ms when readout moved
      to a free function, and a latest-window-end fallback that would
      have always reported t1_ms instead of the real latest window).
- [x] cargo test -p data_plane sketch_db:: -- 294 passed, confirms the
      evaluate_cardinality_global change is behaviorally identical.
- [x] cargo test -p data_plane -- full suite: 902 passed (main lib
      target), same pre-existing unrelated failures as already
      documented on PR #411.
- [x] cargo clippy -p data_plane --lib -- -D warnings -- zero warnings
      in the touched files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… follow-up)

Started as a separate frequency_apply.rs (CMS/CountSketch/heap-bearing
decode+merge), then unified into delta_apply.rs after reconsidering: the
Frequency family's "decode each frame independently, then merge" is the
same fold operation delta_apply.rs's DD/KLL/HLL families already use for
every delta case except two genuine sparse-in-place special cases (DD's
bucket-index proto delta, HLL's register proto delta) -- both already
decode via decode_full-and-merge for their other encodings. So it's one
merge_same_family/cumulative_rolling_state/per_window_rolling_states
pipeline for all six sketch kinds, not two parallel ones.

- RollingState renamed to SummaryState (no longer just the "rolling"
  DD/KLL/HLL families).
- DeltaSketchKind gains Cms/CountSketch/Heap variants (Heap covers both
  CmsWithHeap and CountSketchWithHeap -- they already share one wire
  representation and read out identically in the existing reducer code).
- decode_full/apply_delta_bytes/merge_same_family extended; new
  total()/topk_items() readout accessors.
- summary_executor.rs: dropped the CandidateKind wrapper enum this no
  longer needs -- SidHandle/GroupState carry one DeltaSketchKind
  directly. summary_params_match/to_delta_kind extended for
  Cms/CmsWithHeap/CountSketch/CountSketchWithHeap (width=cols/depth=rows,
  matching the existing wire convention; SketchConfig has no heap_size
  field at all, since heap-bearing kinds reuse their heap-less base's
  config shape for sid identity -- confirmed via
  drivers/ingest/otel.rs's base_sketch_kind_handle).

Also refactored evaluate_cardinality_global (sketch_reducer.rs) to use
SummaryState directly, dropping the redundant cumulative_hll_state
wrapper this obsoletes.

Scope actually covered: quantile/cardinality (unchanged) plus the
Frequency family's BARE total (SketchQuery::PointCount{key:
ColumnRef::SampleValue}) -- no specific item key, both cumulative and
per-window. Explicitly NOT covered, and erroring rather than silently
wrong:
- SketchQuery::TopK -- its answer (K items per timestamp) doesn't fit
  this executor's Value = Vec<(i64, f64)> shape at all; forcing it in
  would silently drop data. Needs a Value type redesign, a separate
  decision.
- PointCount with a named item key -- the value to filter by isn't
  carried by SketchQuery or available in readout's signature at all.

## Test plan
- [x] cargo test -p data_plane --lib summary_executor -- 12 tests (9
      existing + 3 new), all passing:
  - single_cms_sid_total_readout / two_cms_sids_same_group_totals_actually_merge
    -- same two properties proven for DD/KLL earlier (real readout, real
    cross-sid merge via matrix addition), now for CMS.
  - topk_query_is_explicitly_unsupported_not_silently_wrong -- proves
    the scope boundary errors instead of returning a wrong/truncated
    answer.
- [x] cargo test -p data_plane sketch_db:: -- 294 passed, confirms
      evaluate_cardinality_global's refactor is behaviorally identical.
- [x] cargo test -p data_plane -- full suite: 905 passed (main lib
      target), same pre-existing unrelated failures already documented
      on PR #411.
- [x] cargo build --workspace / cargo clippy -p data_plane --lib -- -D
      warnings -- clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ulative/per_window_summary_state

Addresses two new review comments on PR #411.

1. SummaryState::Heap collapsed CmsWithHeap and CountSketchWithHeap into
   one shared variant, reasoning that they share a wire representation
   (asap_sketchlib has no separate CountSketchWithHeap type) and today's
   reducer reads both out identically. That reasoning missed a real bug:
   a CMS-substrate heap and a CountSketch-substrate heap are different
   sketch algorithms that merely happen to share a storage shape --
   merge_same_family's single Heap-Heap arm would have silently allowed
   merging one into the other (mathematically invalid, but type-checks
   fine since both wrap CountMinSketchWithHeap). Split into two distinct
   variants (CmsWithHeap/CountSketchWithHeap), both still backed by
   CountMinSketchWithHeap since that's the only type available, but now
   merge_same_family's per-variant match rejects the cross-family case
   the same way it already rejects e.g. merging a Cms into a Kll.
2. Renamed cumulative_rolling_state -> cumulative_summary_state and
   per_window_rolling_states -> per_window_summary_states, matching the
   RollingState -> SummaryState rename from the prior commit (the
   function names were left stale).

## Test plan
- [x] New test: cms_with_heap_and_count_sketch_with_heap_are_not_the_same_family
      -- proves the fix: merge_same_family now rejects the cross-family
      case with a family-mismatch error instead of silently succeeding.
- [x] cargo test -p data_plane --lib summary_executor -- 12/12 passing,
      unaffected.
- [x] cargo test -p data_plane --lib delta_apply -- 10/10 passing (9
      existing + 1 new).
- [x] cargo test -p data_plane -- full suite: 906 passed (main lib
      target), same pre-existing unrelated failures already documented
      on PR #411.
- [x] cargo clippy -p data_plane --lib -- -D warnings -- clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ummaryState

SummaryState::CountSketchWithHeap wrapped the CMS-family
CountMinSketchWithHeap type as a stand-in, since asap_sketchlib had no
separate CountSketchWithHeap wire type. Now that asap_sketchlib exposes
one (ProjectASAP/asap_sketchlib#78, median-of-signed-rows estimator vs
CMS's min-over-rows), point the variant at the real type: new
decode_cs_with_heap_from_msgpack{,_delta} decoders, and
merge_same_family/decode_full/apply_delta_bytes/total/topk_items updated
accordingly. The two SummaryState variants now hold genuinely different
Rust types, so a cross-family merge is caught by the type system, not
just by the enum-variant check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
decode_frequency_estimate's heap-bearing arm always decoded through
CountMinSketchWithHeap and rebuilt a CountMinSketch (min-over-rows)
regardless of the sid's actual kind -- silently wrong for
CountSketchWithHeap sids (median-of-signed-rows). Same conflation bug
as SummaryState::CountSketchWithHeap in delta_apply.rs (61f3425), in
this separate legacy reducer path. Split the collapsed match arms in
decode_frequency_total, decode_frequency_estimate, and the FrequencyTopk
per-frame heap decode loop to dispatch on the sid's real kind and decode
through the matching asap_sketchlib type.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol
zzylol force-pushed the feat/data-plane-summary-executor branch from 6386113 to 0614b72 Compare July 25, 2026 17:45
@zzylol
zzylol merged commit 97eb892 into main Jul 25, 2026
zzylol added a commit that referenced this pull request Jul 27, 2026
The design table and open questions described the target shape before
implementation started; several turned out to differ once real code got
written. Updates:

- Status note: SummaryExecutor is implemented and tested for
  quantile/cardinality (DDSketch/Kll/Hll), both cumulative and
  per-window, in #411 (unmerged). Frequency family (CMS/CountSketch) in
  progress; ExactAgg readout and wiring execute() into the live serving
  path still not started.
- Table: Handle is SidHandle (sid + already-fetched series + decode
  params), not a bare u64 -- find_candidates needs the series anyway
  for the group key, so it's threaded through rather than re-fetched.
  merge_states/fetch_state are deliberately lazy; the real merge math
  lives in readout, which is the only place that knows
  cumulative-vs-per-window mode.
- Open question 1 (tree source): resolved as both -- the
  control_plane seam for planning-time tree construction, plus a
  direct asap-sketch/asap-ir dependency for everything serving-time
  (implementing the trait / matching its types requires importing the
  defining crate directly, not just consuming a function that returns
  those types).
- Open question 2: partially resolved -- KLL/DDSketch/HLL merge done
  (via a generalized existing HLL-only primitive, not built from
  scratch), CMS/CountSketch still open. Partial-group-coverage question
  resolved (fold whatever's present). Accuracy-math and
  resize/downsample questions remain genuinely open.
- Questions 3-4 unchanged -- neither addressed yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol
zzylol deleted the feat/data-plane-summary-executor branch September 12, 2026 14:48
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