Skip to content

fix(emit): B3-pop + B4-window bundle (B2-Sum deferred) - #282

Merged
zzylol merged 1 commit into
mainfrom
b3-pop-b4-window-b2-sum-bundle
May 18, 2026
Merged

zzylol merged 1 commit into
mainfrom
b3-pop-b4-window-b2-sum-bundle

Conversation

@zzylol

@zzylol zzylol commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Two control-plane follow-ups bundled into one PR (third intended fix deferred — see below).

  • Fix feat: PrecomputeJob execution endpoint for DataCollector integration #1 (B3 population gap): emitted transform/keep_for_<metric> blocks had empty keep_keys lists, stripping ALL wire attrs instead of preserving the streaming-config's grouping labels. Root cause: the canonical MVP query quantile_over_time(0.99, http_requests_total_latency_ms[30s]) carries no PromQL by (...) clause, and the pre-pop loop in main.rs passed group_by_labels: vec![] in the QuerySpec, so QueryWorkload.group_by_labels ended up empty. Fix: add a declarative grouping_labels: Vec<String> field to WorkloadEntry and thread it through the analyzer into the typed emit path.
  • Fix feat: DataCollector controller client — dynamic query config #2 (B4 window-size from query): agent sketch window_duration was hardcoded at 300s / 60s independent of the workload's [range]. Root cause: the pre-pop QuerySpec hardcoded time_window: \"5m\", overriding the parser. Fix: drive time_window from the parsed PromQL when query_string is present, and add a clamp_window_secs helper in emit/stage_config.rs with [5, 60] bounds applied at every emission site (sketch processor's window_duration AND streaming-config's windowSize) so the agent and backend keep the same window.

Deferred

Test plan

…-Sum deferred)

Two fixes that all live in control_plane/src/emit/ and adjacent
files, bundled to avoid sequential merge churn. The third
intended fix (B2 — Sum aggregation alongside sketch) was
deferred because the existing workload_store keys by metric name
and stores ONE QueryWorkload per metric — the multi-aggregation
shape needed for `sum by (zone)` alongside `quantile_over_time`
requires either a store-level restructure or a second
PhysicalExpr per metric, both substantially bigger than #1+#2.
Left as a follow-up; the YAML's existing sum-shaped entries
(MVP entries 2/3/4 targeting http_requests_total) still collapse
to the last write on a per-metric basis.

== Fix #1: B3 population gap ==

Symptom (smoke test): every emitted `transform/keep_for_<metric>`
block had `keep_keys(datapoint.attributes, [])` — strips ALL
attrs instead of keeping `grouping_labels`. Sid catalog landed
with one sid per metric instead of one per (metric, zone).

Root cause: the OpAMP-push path's `metric_to_grouping_labels`
WAS being populated from the WorkloadStore (in `main::
emit_bootstrap_typed`, `main::handle_plan`'s typed-stage-split
branch, and `replan::Replanner::try_emit_typed_edge_yaml`). But
the source `QueryWorkload.group_by_labels` was empty for the
canonical MVP query `quantile_over_time(0.99,
http_requests_total_latency_ms[30s])` — the PromQL parser only
surfaces grouping labels from `by (...)` clauses, and a bare
`quantile_over_time` has none. The pre-pop loop in `main.rs`
also passed `group_by_labels: vec![]` in the QuerySpec, leaving
nothing to merge with the empty parsed value.

Fix: add a declarative `grouping_labels: Vec<String>` field to
`WorkloadEntry` so the YAML can state the streaming-config
grouping contract directly. Thread `entry.grouping_labels` into
`QuerySpec.group_by_labels` in the pre-pop loop (main.rs ~276)
so `analyzer.analyze()` merges the YAML-declared labels with any
PromQL `by` keys into `QueryWorkload.group_by_labels`, which
`collect_metric_to_grouping_labels` then drops into
`EdgeStageConfig.metric_to_grouping_labels` → the emitter's
`keep_keys(datapoint.attributes, [...])` list. Same plumbing in
`emit::runtime_tests::populate_store_from_registry` so the
existing 5-sketch round-trip test keeps tracking main.rs.

Regression coverage (2 new tests in emit/mod.rs):
  * workload_entry_grouping_labels_round_trip_through_emit_to_keep_keys
  * workload_entry_grouping_labels_surface_in_emit_keep_keys_list

== Fix #2: B4 — controller picks window_duration from query ==

Symptom: agent's sketch processor's `window_duration` was
hardcoded at 300s (5m) for KLL/CMS/etc., 60s for others. The
backend's streaming-config `windowSize` likewise drifted from
whatever the user wrote in their PromQL `[range]`. Queries with
`[30s]` ranges always landed inside an open sketch window and
returned NoData.

Root cause: the pre-pop QuerySpec hardcoded
`time_window: "5m".into()` (main.rs ~278), which the analyzer
prefers over the PromQL-parsed value at pipeline.rs:203. The
parsed `[30s]` was thrown away. No clamp existed downstream to
catch this either, so a `[5m]` workload landed a 300s sketch
window that fell outside every sensible replay range.

Fix:
  1. Pass `time_window: ""` from main.rs and emit/runtime_tests
     when the entry HAS a `query_string` — lets the analyzer
     extract the matrix-selector range itself. Falls back to
     "5m" only when query_string is None (so the analyzer
     doesn't error at Step 4).
  2. Add `clamp_window_secs(Option<u64>) -> Option<u64>` in
     emit/stage_config.rs with bounds [5, 60]:
       * Lower 5s — below this the sketch processor mints new
         windows before it has enough samples for the family's
         quality bound, and per-flush sid-catalog cardinality
         explodes.
       * Upper 60s (= MAX_WINDOW_SECS, the historical default).
         Above this the user's replay range no longer contains
         a closed sketch window.
  3. Apply the clamp at every emission site so the agent's
     `window_duration` and the backend's `windowSize` agree
     exactly (drift de-syncs warm-tier replay):
       * `build_edge_processor_block` callers in the legacy
         single-pipeline and 5-sketch routing emit paths
         (stage_config.rs ~169 and ~1003).
       * `build_backend_aggregation_json` (stage_config.rs
         ~1636 — the streaming-config JSON path).
       * `generate_streaming_config_yaml` (asapquery_backend.rs
         — the legacy YAML emit path; same clamp so legacy /
         typed paths agree).
       * `build_processor_block` in the legacy agent emitter
         (agent.rs ~138).

Regression coverage:
  * 4 unit tests for `clamp_window_secs` itself (in-range,
    above-max, below-min, None).
  * 4 emit-level tests: legacy edge-yaml clamps 300 → 60,
    legacy edge-yaml preserves 30, 5-sketch routing clamps
    across all 5 family processors, streaming-config JSON
    clamps `windowSize`.
  * 3 legacy `agent.rs` tests covering the same clamp
    contract (clamps_oversize, clamps_undersize, preserves_inrange).
  * 1 pre-existing test (`contains_window_duration`) updated
    to assert the post-clamp 60s value instead of the
    fixture's pre-clamp 5m.

Test plan:
  * `cargo test -p control_plane --lib`: 719 pass (was 706
    pre-change baseline; +13 new tests covering both fixes)
  * `cargo test -p data_plane --lib`: 712 pass (no data_plane
    changes — verification only)
  * `cargo check -p control_plane`: clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 9d765d8 into main May 18, 2026
zzylol added a commit that referenced this pull request May 18, 2026
…ti-role metrics (B2 full restructure) (#283)

Closes the B2 thread (Sum-as-separate-aggregation alongside
sketch). PR #282's bundle deferred this with note "needs
(metric, role)-keyed restructure"; the B2-planning-agent's
analysis concluded the restructure is the architecturally clean
solution despite its size (vs the side-table alternative).

What changed:

  * New AggRole enum (Quantile / Sum / Count / Topk / Other) in
    control_plane/src/workload.rs + derive_agg_role() classifier
    that resolves from (a) sketch_family_override (DDSketch/KLL →
    Quantile, HLL → Count, CountSketch → Topk, CMS → Other) then
    (b) outermost PromQL token of query_string.
  * WorkloadStore keyed by (metric: String, role: AggRole) —
    append-not-collapse on duplicate metric inserts. New
    `get_all_for_metric` + `keys()` accessors for emit-path
    walks.
  * PlanStore keyed by (metric, role) — one CollectionPlan per
    pair; rollback/diff/expired now per-pair. `metrics()` dedups
    across roles to preserve the metrics-exposer wire shape.
  * agent_to_metric → agent_to_metrics: Vec<(metric, role)> in
    replan.rs so a single agent can serve multiple (metric,
    role) plans. register_agent / unregister_agent updated;
    push_config_to_agent picks the first pair (the 5-sketch
    routing-connector edge YAML carries every metric's pipeline
    anyway, so one push covers them all).
  * Replanner::replan_metric stays as a wrapper that loops over
    every role registered for the metric; new
    replan_metric_role(metric, role) for targeted single-role
    replans. replan_expired loops over expired (metric, role)
    pairs; handle_violation re-plans every pair the agent serves.
  * main.rs pre-pop loop now derives the role per WorkloadEntry
    via derive_agg_role() before set — the three
    http_requests_total entries in mvp-workload.yaml (sum / sum+
    rate / count) now persist as TWO distinct keys
    (http_requests_total, Sum) + (http_requests_total, Count)
    instead of collapsing onto one with only the last entry's
    plan surviving.
  * handle_plan derives the role from the QuerySpec's
    query_string + optional sketch_type override and threads it
    into store.set / workload_store.set / register_agent.
  * HTTP endpoints (/api/v1/plan/:metric, /diff,
    /rollback, /config) preserved at metric granularity;
    responses extended with a per-role `roles: [...]` array
    (additive, no breaking URL change). Rollback returns
    BAD_REQUEST when no role has a previous plan (matches
    pre-B2 single-role behaviour).
  * Emit helpers (collect_metric_to_family,
    collect_metric_to_grouping_labels) walk the new (metric,
    role) keyed store and take the FIRST sketch-binding role
    per metric for the routing-connector OTTL conditions —
    Sum-shaped roles correctly decline bind_workload_typed and
    flow through the metrics/raw_passthrough pipeline.
  * Bootstrap typed path (emit_bootstrap_typed) walks every
    role's workload until one binds, instead of the prior
    single-role lookup.
  * metrics_exposer's refresh_plan_ids iterates per-pair so
    each (metric, role) emits its own asap_active_plan_id row
    under the metric label. Role folded into the plan_id hash
    so distinct-role plans produce distinct ids.

Test plan:

  * cargo test -p control_plane --lib: 740 pass (was 719; +21
    new tests covering AggRole classification, WorkloadStore
    multi-role coexistence, PlanStore role-keyed
    rollback/diff/expired, Replanner replan_metric_role +
    multi-role agent registration + violation handling, and the
    synthetic http_requests_total 3-entry regression test).
  * cargo test -p control_plane --bins: 27 pass (unchanged).
  * cargo test -p data_plane --lib: 712 pass (no data_plane
    changes — backend's ingest_precompute_for_agg_config
    already handles AggKind::ExactAgg(Sum)).

Smoke test (NOT run by this commit):
  The mvp-smoke-test/run_smoke.sh end-to-end check requires
  docker compose + the full ASAP stack + a backend-streaming.yaml
  that includes a Sum-shaped aggregationType entry for
  http_requests_total. The static backend-streaming.yaml in
  deploy/configs/ today only carries DDSketch entries — that's
  a deployment-config follow-up, NOT a controller change.
  Without the static YAML update the data_plane has no Sum
  capability for http_requests_total at startup and the
  `sum by (zone)` PromQL still surfaces capability-miss until
  the dynamic streaming-config POST path (Phase 5 controller →
  backend hot-reload) plumbs through to the live backend.

Ambiguous-case decisions (documented inline):
  * count_over_time(...) classifies to Count (cardinality
    semantics). count(over_time(sum_over_time(...))) shape
    would be Sum via the outer aggregation.
  * rate / increase classify to Sum (both bind to
    ExactAgg(Sum) on the data plane).
  * CountMinSketch override classifies to Other (frequency
    estimator without an inherent topk shape — keep Topk pure
    for sketch-with-heap families).

Closes B2.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol deleted the b3-pop-b4-window-b2-sum-bundle branch July 17, 2026 20:04
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