Skip to content

refactor(emit): align typed L5 streaming-config JSON with backend parser - #244

Merged
zzylol merged 1 commit into
mainfrom
align-streaming-config-shape
May 15, 2026
Merged

zzylol merged 1 commit into
mainfrom
align-streaming-config-shape

Conversation

@zzylol

@zzylol zzylol commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

The typed L5 path's emit_backend_streaming_config_json was emitting a stripped-down JSON shape that didn't parse on the backend — every handle_plan POST to /api/v1/streaming-config was failing HTTP 400 (Missing grouping labels / Missing windowSize / Missing metric / …). The deploy ran ”OK” only because the Replanner's separate legacy emitter (generate_streaming_config_yaml) posts valid YAML on each replan — but on a fresh deploy, the backend had no aggregations registered from the controller until the first replan fired.

This PR closes the gap:

  • Adds content fields to BackendAggregationmetric_name, window_secs, spatial_filter. Populated from the surrounding EdgeStageConfig at the two construction sites (SketchAgg @ Edge, RawAtEdgeSketchAtBackend @ Edge).
  • Rewrites build_backend_aggregation_json to emit the full backend-expected shape (aggregationType, aggregationSubType, metric, labels.{grouping,rollup,aggregated}, parameters, windowSize, windowType, spatialFilter, aggregationInput).
  • Drops aggregationId from the wire entirely (PR 5 alignment — the backend's parser silently dropped it anyway; identity is content-addressed via PolicyFingerprint(u64)). aggregation_id remains on the struct as internal emitter plumbing for the SketchAgg → BackendAggregation → BackendReadout cross-reference during DAG walk.

ID design philosophy this PR commits to

Per the design discussion: the control plane emits content, not derived identifiers. The data_plane derives the PolicyFingerprint(u64) from the same content; sids (per-series) are minted by SeriesIdResolver at ingest. The controller never participates in either u64 namespace — same content → same fingerprint, by construction.

Out of scope (follow-ups)

  • labels.grouping is emitted empty in this PR. Threading QueryExpr::Aggregate.by through BackendAggregation requires column-id-to-name resolution via the schema — separate concern. The backend's parser accepts the empty list, so this is correct-but-incomplete content fidelity.
  • Readouts' aggregationId stays for now. The backend's StreamingConfig::from_yaml_data ignores the entire readouts list today, so the field is informational. Cleanup belongs in whatever PR wires the readouts up.
  • ASAPCollector static fixtures (deploy/mvp-singlenode/configs/backend-streaming-{kll,cs,hll}.yaml) still carry aggregationId: N lines — tiny separate PR.

Test plan

  • cargo check clean (only pre-existing warnings)
  • cargo test --lib: 686 passed; 0 failed
  • cargo test --tests --bins: 27 passed; 0 failed
  • 9 fixtures updated to the new shape; 2 assertions flipped from ”aggregationId present” → ”aggregationId absent + metric/windowSize present”.

🤖 Generated with Claude Code

The typed L5 path's `emit_backend_streaming_config_json` was emitting
a stripped-down JSON shape that did not parse on the backend — every
`handle_plan` POST to `/api/v1/streaming-config` was failing with
HTTP 400 (`Missing grouping labels` / `Missing windowSize` / …). The
runtime tolerated this because the Replanner's separate legacy
emitter (`generate_streaming_config_yaml` in `emit/asapquery_backend.rs`)
posts valid YAML on each replan; but on a fresh deploy the backend had
no aggregations registered from the controller until the first replan.

This PR aligns the typed L5 wire shape with what
`asap_types::AggregationConfig::from_yaml_data` requires:

  * Adds `metric_name`, `window_secs`, `spatial_filter` fields to
    `BackendAggregation` (populated from the surrounding
    `EdgeStageConfig` at the two construction sites — for
    `SketchAgg @ Edge` and `RawAtEdgeSketchAtBackend @ Edge`).
  * Rewrites `build_backend_aggregation_json` to emit the full
    backend-expected shape: `aggregationType`, `aggregationSubType`,
    `metric`, `labels.{grouping,rollup,aggregated}`, `parameters`,
    `windowSize`, `windowType`, `spatialFilter`, `aggregationInput`.

PR 5 alignment: drops `aggregationId` from the wire entirely (it's
silently dropped by the backend parser anyway — identity is
content-addressed via `PolicyFingerprint(u64)` derived from the fields
above). The `aggregation_id` field remains on the struct as internal
emitter plumbing (`SketchAgg → BackendAggregation → BackendReadout`
cross-reference during the DAG walk), but never reaches the wire.

`labels.grouping` is emitted empty in this PR — the typed L5 doesn't
thread `QueryExpr::Aggregate.by` through `BackendAggregation` yet.
That's a follow-up; the parser accepts the empty list. `spatial_filter`
is populated from `edge.label_filters` (the `Scan` label filters
extracted by `extract_edge_facts`). `windowSize` comes from
`edge.window_secs` (the `Window` op extracted by the same).

Test updates:
- 9 fixtures updated to construct `BackendAggregation` with the new
  fields.
- 2 assertions flipped from "aggregationId is present" to
  "aggregationId is absent + metric/windowSize present".
- `backend_client.rs` mock-POST bodies updated to current shape.
- 1 helper added: `spatial_filter_from_label_filters`.

Build: clean. 686 lib + 27 binary tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 55a7b82 into main May 15, 2026
zzylol added a commit that referenced this pull request May 15, 2026
…els.grouping (#245)

Follow-up to #244. The typed L5's `BackendAggregation` now carries a
`grouping: Vec<String>` field, surfaced under `labels.grouping` in the
streaming-config JSON the controller posts to asapquery-backend. The
backend's precompute-engine accumulator pipeline keys its
per-aggregation state by the projected attribute set, so this is what
makes cross-host fan-in merges (sum by zone / etc.) actually behave
correctly on the backend side.

## Plumbing rationale

The L3 `QueryExpr::Aggregate.by` is `Vec<ColumnId>` — positional
indexes into a synthesized `Schema` that intentionally does not track
open-set labels (see the module doc on
`intent_algebra::column_resolution` — label-set resolution is a
Step γ TODO). So reverse-resolving ColumnId → label name at L5 emit
time isn't tractable.

`QueryWorkload.group_by_labels: Vec<String>` carries the names
unambiguously, and `handle_plan` already has the workload in scope at
the call site for both binder paths (query_string → `bind_query_expr`
and explicit-field → `bind_workload_typed`). The pragmatic plumb:

1. Add `grouping: Vec<String>` to `BackendAggregation` (default empty;
   `#[serde(default)]` keeps existing serialised fixtures parsing).
2. Have the L5 emitter populate `grouping: vec![]` at both
   construction sites (no behaviour change at emit time).
3. In `handle_plan`, after `split_typed_three_stage` returns the
   per-stage map, patch every `BackendAggregation.grouping` with
   `workload.group_by_labels.clone()` before
   `emit_backend_streaming_config_json`. Every aggregation under a
   single workload shares the same grouping today, so the patch is
   uniform.
4. `build_backend_aggregation_json` reads `agg.grouping` into
   `labels.grouping` (was hard-coded empty before this PR).

`labels.rollup` and `labels.aggregated` stay empty — the controller
doesn't surface either today.

## Test plan

- New: `backend_json_emits_grouping_under_labels` exercises the JSON
  round-trip of the new field directly. 687 lib tests (+1) + 27
  binary tests pass.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 15, 2026
#246)

Mirror of #244's aggregation-side cleanup, now for readouts. The
backend's `StreamingConfig::from_yaml_data` doesn't consume the
`readouts` list at all today, so this is wire-shape hygiene rather
than a fix — but it keeps the controller's emit consistent with the
content-addressing convention PR 5 established: no controller-allocated
string IDs on the wire.

When the backend eventually starts consuming readouts, the cross-
reference from a readout to its source aggregation will be content-
shaped (metric / sketch_kind / params) — derivable from the
`aggregations` list by the same `PolicyFingerprint` recipe. The
`BackendReadout.aggregation_id` struct field stays in source as
emit-time bookkeeping (populated by `resolve_descendant_agg_id_via_edges`
during the DAG walk; not consumed by anything downstream today).

Touches:
- `build_backend_readout_json`: 4 match arms each drop their
  `aggregationId` key. Doc-comment expanded to explain the
  content-addressing convention.
- `phase_b_backend_json_aggregation_readout_alias_snapshot`: flips
  the readouts assertion from "aggregationId present" → "absent".

Build clean. 687 lib + 27 binary tests pass.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 15, 2026
…g plumb (#247)

First end-to-end test of the gateway-less control-plane ↔ data-plane
contract. Lives in `data_plane/tests/` since data_plane already owns
the OTLP/precompute/HTTP wiring; control_plane is already a
workspace dep so no Cargo.toml changes were needed.

## What it tests

Two `#[tokio::test]`s:

1. `controller_plans_ddsketch_quantile_and_backend_parses_streaming_config`
   — workload `{ metric: http_latency_ms, agg: Quantile, sla: 0.01,
   window: 60s }` flows through `bind_workload_typed` →
   `split_typed_three_stage` → `emit_backend_streaming_config_json`,
   the JSON is POSTed to the in-process backend's
   `/api/v1/streaming-config`, and the GET endpoint reflects the
   parsed aggregation. Asserts the emitted JSON's shape (#244 content
   fields present, #244/#246 `aggregationId` absent).

2. `controller_plans_with_grouping_and_backend_parses_grouping_labels`
   — same shape but with `group_by_labels: ["zone"]`. Asserts the
   emitter surfaces `labels.grouping: ["zone"]` (#245), the POST
   succeeds, and the active-config snapshot contains `"zone"` in the
   parsed `AggregationConfig.grouping_labels`.

## Findings the test caught

`bind_workload_typed`-produced PhysicalExpr doesn't surface
`metric_name` or `window_secs` to `BackendAggregation` via
`extract_edge_facts`. The underlying cause is likely that the
allocator paints the `Logical(Scan{...})` chain at a stage other than
Edge in some binder outputs, so the walk's `(Logical, Edge)` match arm
doesn't fire — Step γ open-set label work (also blocking grouping
fidelity) probably needs a coordinated fix. Pragmatic workaround
mirroring the #245 grouping patch:

  control_plane/src/main.rs (handle_plan, Backend stage match arm)
    for agg in &mut be.aggregations {
        if agg.metric_name.is_empty() {
            agg.metric_name = workload.metric_name.clone();
        }
        if agg.window_secs == 0 {
            agg.window_secs = workload.time_window.as_secs();
        }
        agg.grouping = workload.group_by_labels.clone();
    }

This is belt-and-braces — if the L5 walk DID surface the field,
the conditional `if … is_empty()` / `== 0` checks preserve it;
otherwise the workload spec wins. Closes a real bug where fresh-deploy
`handle_plan` POSTs would have failed the backend parser on
`Missing metric` / `Missing windowSize` (PR #244 caught the JSON shape
gap; this test caught the value-population gap).

## Other change

`HttpServer::start_test_server` had `#[cfg(test)]` gating it to the
lib's own unit tests — integration tests under `data_plane/tests/`
are compiled separately and couldn't see it. Dropped the gate; the
method's name + doc-comment make the test-only intent explicit, and
production code uses `start()` regardless.

## Test plan

- [x] `cargo test --test e2e_controller_plans_and_backend_serves`:
  **2 passed**.
- [x] `cargo test --lib -p control_plane`: **687 passed**.
- [x] `cargo test --tests --bins -p control_plane`: **27 passed**.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 15, 2026
…re (#251)

Mirrors #244 / #246 / #250's PR-5 wire-cleanup pattern, now for the
edge-side runtime emitters. The patched asap-otel processors (any
runtime — OTel collector / OTAP / Telegraf) don't consume the
controller-allocated `aggregation_id` string: sid identity is
content-addressed at the backend via `(metric_name,
attrs_fingerprint, agg_kind_canonical)`, and `policy_fp` content-
matches via `(metric, sketch_kind, config, group_by_keys)`. The
controller-allocated string is dead weight on the wire.

The OTel-collector YAML emitter (`emit_edge_yaml`) was already
clean — there's an existing test at stage_config.rs asserting the
agent YAML does NOT contain `aggregation_id:`. This PR catches the
two remaining edge-runtime emitters that still spelled it out:

- `emit/otap.rs::build_asap_sketches_config` (line ~287): drop
  the `aggregation_id` map entry; keep `sketch_kind` etc.
- `emit/telegraf.rs::emit_processors_allsketches` (line ~158):
  drop the `aggregation_id = "…"` TOML line; keep `sketch_kind`.

`EdgeSketchProcessor.aggregation_id` stays on the struct as
internal emitter plumbing for cross-stage references during the
DAG walk (`SketchAgg → BackendAggregation → BackendReadout`); it
just doesn't reach the wire from any runtime emitter anymore.

Tests: 690 lib + 27 binary tests pass. (No test asserted the field
was present in OTAP / Telegraf output, so nothing to flip.)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol deleted the align-streaming-config-shape 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