Skip to content

mvp v6 phase B: controller plan -> per-stage runtime config emitter - #297

Merged
zzylol merged 3 commits into
mainfrom
mvp/v6-phase-b-controller-emitter
May 6, 2026
Merged

zzylol merged 3 commits into
mainfrom
mvp/v6-phase-b-controller-emitter

Conversation

@zzylol

@zzylol zzylol commented May 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase B of the MVP v6 plan, per the Phase A audit.

The controller's StageAllocator + ThreeStageEmitter already produce a structured HashMap<StageId, StageConfig> (in controller/src/stage_split/emitter.rs), but split_typed_three_stage was #[allow(dead_code)] with no callers. This PR adds the per-stage emitter that materialises each StageConfig variant into wire bytes and wires it into handle_plan behind a feature flag.

New module — controller/src/config/stage_config.rs

Three pure functions, modelled after the existing config::agent / config::backend patterns:

  • emit_edge_yaml(&EdgeStageConfig, opamp_endpoint) -> Result<String> — OTel YAML for the edge agent: OTLP receiver on :4317/:4318, one processor per EdgeSketchProcessor (kllprocessor, ddsketchprocessor, hllprocessor, countminsketchprocessor, countsketchprocessor), OTLP exporter to gateway. Window / label-filter / aggregation_id / family-specific params are threaded through from the typed config.
  • emit_gateway_yaml(&GatewayStageConfig, opamp_endpoint) -> Result<String> — OTel YAML for the gateway: OTLP receiver on the configured port, family-specific merge processor (kllmerge, ddsketchmerge, hllmerge, countminmerge, countsketchmerge) per GatewayMergeProcessor, OTLP exporter to backend.
  • emit_backend_config_json(&BackendStageConfig) -> Result<serde_json::Value> — JSON document matching the ASAPQuery-backend POST /api/v1/streaming-config API surface; mirrors config::asapquery_backend::generate_streaming_config_yaml's shape, sourced from the typed L5 BackendStageConfig instead of a CollectionPlan.

8 unit tests cover round-tripping aggregations / readouts, processor / pipeline key consistency, batch-vs-window mode, family-specific param surfaces, and ExportTarget::Endpoint passthrough.

Wiring — controller/src/main.rs

After the legacy plan + push, handle_plan consults planner::stage_split::typed_stage_split_enabled() (USE_TYPED_STAGE_SPLIT=1). When set, it binds the workload to a SketchExpr, runs split_typed_three_stage, and for each (StageId, StageConfig) pair calls the matching emitter:

  • EdgeOpampServer::push_to_role(AgentRole::Agent, ...)
  • Gatewayinfo! log of the YAML (no AgentRole::Gateway exists today; Phase C adds the role + push)
  • Backendinfo! log of the JSON (Phase C wires the actual POST through the existing BackendClient)

Phase A audit verdict honoured

  • Worktree-only, mvp/v6-phase-b-controller-emitter branched off origin/main.
  • No deploy/configs or deploy/scripts touched.
  • Postfix-owned files untouched; v5-author files untouched.
  • Behind feature flag — existing controller behaviour unchanged unless USE_TYPED_STAGE_SPLIT=1.

Notes for Phase C

  • Env-var name: USE_TYPED_STAGE_SPLIT=1 (already declared as ENV_USE_TYPED_STAGE_SPLIT in planner::stage_split).
  • Gateway role gap: AgentRole only carries Agent / Backend today. Phase C should add AgentRole::Gateway so emit_gateway_yaml output can be pushed via OpAMP rather than logged.
  • Backend JSON push: backend_client::BackendClient is currently held only by the Replanner. Phase C should plumb it onto AppState so handle_plan can call client.push_streaming_config_json(...) directly.
  • Endpoint resolution: ExportTarget::Stage(_) resolves to documented placeholders (gateway:4317, backend:4317); Phase C plumbs DeploymentConstraints::executors() for real addresses.
  • Processor naming convention (matches the patched opentelemetry-collector-contrib-patch/processor/):
    • Edge: kllprocessor, ddsketchprocessor, hllprocessor, countminsketchprocessor, countsketchprocessor
    • Gateway: kllmerge, ddsketchmerge, hllmerge, countminmerge, countsketchmerge
  • Backend type strings: DDSketch, DatasketchesKLL, HLL, CountSketch, CountMinSketch — same mapping config::asapquery_backend::map_sketch_type_to_agg_type already uses.

Test plan

  • cargo test stage_config -p controller — 8 new tests pass.
  • cargo test -p controller — 475 pass (467 baseline + 8 new); the 10 pre-existing failures (opamp::tests::push_to_*role*, api_tests::*, analyzer::tests::valid_spec, etc.) reproduce on bare origin/main and are unrelated.
  • cargo check -p controller clean.
  • Phase C overlay smoke-test (deferred — Phase C work).

LOC: ~720 in the new emitter file (functions + 8 tests + doc), ~85 lines of main.rs wiring + planner doc updates. 5 files touched (1 new, 4 modified).

🤖 Generated with Claude Code

zzylol and others added 3 commits May 6, 2026 16:53
Per-stage emitter that turns each StageConfig variant produced by
ThreeStageEmitter into the wire bytes the executor consumes:

  * emit_edge_yaml      — OTel YAML for the edge agent (OTLP
    receiver, per-sketch processor block, OTLP exporter to gateway).
  * emit_gateway_yaml   — OTel YAML for the gateway (OTLP receiver,
    family-specific *merge processor block, OTLP exporter to backend).
  * emit_backend_config_json — JSON document matching the
    ASAPQuery-backend POST /api/v1/streaming-config surface.

8 new unit tests cover round-tripping aggregations / readouts,
processor / pipeline key consistency, batch-vs-window mode, and
sketch-kind → backend-type mapping. Pure transformation; no I/O.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(Phase B step 2)

Drop the #[allow(dead_code)] gates on split_typed_three_stage,
typed_stage_split_enabled, and bind_workload_typed — Phase B's
main::handle_plan wiring (next commit) is their first caller, so the
suppressions are stale.

Also points the split_typed_three_stage docstring at the new
config::stage_config emitter module so future readers can follow the
typed L5 pipeline end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…TYPED_STAGE_SPLIT (Phase B step 3)

When USE_TYPED_STAGE_SPLIT=1 is set, after the legacy plan push,
handle_plan additionally:

  1. binds the workload to a SketchExpr via planner::rules::bind_workload_typed,
  2. runs the typed L5 path via planner::stage_split::split_typed_three_stage,
  3. for each (StageId, StageConfig) entry, calls the matching emitter
     in config::stage_config and routes the output:
       - Edge:    pushes the YAML to OpAMP role-Agent.
       - Gateway: logs the YAML at info! (no AgentRole::Gateway yet —
                  Phase C adds it + a proper push).
       - Backend: logs the JSON at info! (Phase C wires the streaming-
                  config POST through the existing BackendClient).

Existing controller behaviour is unchanged unless the env var is set;
the legacy untyped path still runs in both branches so nothing
regresses for callers that don't opt in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 516115b into main May 6, 2026
@zzylol
zzylol deleted the mvp/v6-phase-b-controller-emitter branch May 6, 2026 20:54
zzylol added a commit that referenced this pull request May 7, 2026
…314)

The controller's L1→L5 pipeline (PRs #273-279) plans sketch placement,
and Phase B (PR #297) added `emit_backend_config_json` for the backend's
StreamingConfig. The per-metric `BackendStorageRouting` table that the
backend's HTTP query handler consults on every PromQL query, however,
was still hand-authored YAML at `deploy/configs/backend-storage-routing.yaml`
— two sources of truth, drift between the controller's stage-split
decisions and the routing config, and manual edits required when the
workload changes.

Phase α makes the controller THE planner: it emits a
`BackendStorageRouting` JSON document as part of every plan emit, the
backend hot-loads it on push, and routing flows via OpAMP push instead
of YAML edits.

Concretely:

* `config::stage_config::emit_backend_storage_routing(metric_plans)` —
  for each `(metric, &BackendStageConfig)` pair the controller has
  planned this cycle, emit a `metrics:` row with the per-shape engine
  routing list. Classification rules are sourced from the L4
  `sketch_algebra` outputs landing at the backend (DDSketch / KLL →
  warm tier for `quantile`; HLL → warm tier for `count`; Count-Sketch
  → warm tier for `topk`; CMS → warm tier for `count` / `point_count`).
  Archive-eligible shapes — `histogram_quantile`, `delta`, `deriv`,
  `absent`, `rate_post_hoc`, plus `topk` / `count` when no sketch
  claims them — are emitted on a `thanos_archive` target with an
  explicit `applies_to_query_shape` filter. Warm-tier slot stays the
  default (no filter) so unanticipated shapes route to warm rather
  than failing through to the archive's first-target fallback.

* `BackendClient::post_storage_routing_json` — sibling of
  `post_streaming_config_json`. Rewrites the configured streaming-config
  endpoint URL's path component from `/api/v1/streaming-config` to
  `/api/v1/storage_routing` so operators only configure one
  `CONTROLLER_BACKEND_ENDPOINT` and both pushes land at the same backend
  host.

* `main::handle_plan` — when `USE_TYPED_STAGE_SPLIT=1` and the typed L5
  emitter produced a `BackendStageConfig`, also call
  `emit_backend_storage_routing` and POST it via the shared
  `BackendClient`. Same fire-and-forget contract as the existing
  `streaming-config` push: errors logged at WARN, the next replan cycle
  retries.

Tests: 11 new tests (7 emitter unit / snapshot tests, 2 URL-derivation
tests, 2 mock-backend integration tests). Snapshot test in
`storage_routing_three_metric_snapshot_stable` pins the exact JSON
shape for a DDSketch + HLL + Count-Sketch plan so accidental schema
drift surfaces immediately. Pre-existing 10 controller failures
unchanged (493 pass, was 482+).

Phase α is gated behind `USE_TYPED_STAGE_SPLIT=1` (the existing typed
L5 path). Operators can still hand-author
`deploy/configs/backend-storage-routing.yaml` for dev / standalone
deployments — the backend falls back to the static YAML when no JSON
has been pushed yet (Part B, separate PR).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 19, 2026
…W config + per-series quantile

Bundle of supporting changes for apples-to-apples accuracy validation
of asap-tier vs baseline (b0/b1 VictoriaMetrics). Lands alongside the
quantile-shape + cumulativetodelta engine fixes (ASAPQuery-backend
PR #297 + #299, ASAPCollector cumulativetodelta commit above).

Changes:

1. **fake-exporter/main.go** — EXPORTER_SEED env var seeds per-series
   PRNG (math/rand) deterministically so all 3 arms in a sequential
   run emit identical latency value sequences. Without it, cross-arm
   sampling noise masked DDSketch ε behavior in the accuracy report
   (b0 vs b1 differed by 0.04-0.4% just from random draws, not engine
   error). Default seed of 42 in run_demo.sh; back-compat when unset
   (auto-random, original behavior). Per-series PRNG uses
   `seed ^ hash(EXPORTER_PRODUCER_ID) ^ (seriesIdx+1)*prime` so
   distinct producers + series get distinct sequences.

2. **b0/b1 PRW exporter config — add_metric_suffixes: false** —
   VictoriaMetrics' OTLP→PRW path appends `_milliseconds` to metric
   names with `WithUnit("ms")` annotation (Prometheus naming convention).
   Asap tier preserves the original name. Without this fix, the
   accuracy comparison probe queries `http_requests_total_latency_ms`
   land on different metric names per tier — apples-to-oranges.

3. **mvp-workload.yaml** (singlenode + multinode) — dropped
   `grouping_labels: [zone]` from quantile-metric entries
   (http_requests_total_latency_ms, request_size_bytes). Per-series
   DDSketch / KLL sketches preserve PromQL's per-series semantics so
   `quantile_over_time(...)` returns comparable per-series rows in
   both asap and baseline. Counter-metric entries retain
   grouping_labels: [zone] (Sum aggregation is mergeable; no semantic
   asymmetry there). The KLL override on the latency entry is also
   tracked here for the DDSketch-vs-KLL accuracy comparison documented
   in mvp_smoke_test_findings.md.

4. **run_demo.sh** — pass EXPORTER_SEED=42 (default) to all producers.

Accuracy results post all fixes (multinode all-arms):
- p50 quantile: 0.4-0.6% rel-err vs baseline ✅
- p99 quantile: 11-12% rel-err (DDSketch ε + temporal-window variance)
- max by (zone) (quantile_over_time(...)): 8-9% rel-err (DDSketch)
- sum-by-zone/rate/topk: post-#299 returns delta-window semantics
  (not directly comparable to baseline cumulative as raw numbers, but
  bug-correct; ratio drops from ~300× to ~1× when normalized).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 19, 2026
…W config + per-series quantile

Bundle of supporting changes for apples-to-apples accuracy validation
of asap-tier vs baseline (b0/b1 VictoriaMetrics). Lands alongside the
quantile-shape + cumulativetodelta engine fixes (ASAPQuery-backend
PR #297 + #299, ASAPCollector cumulativetodelta commit above).

Changes:

1. **fake-exporter/main.go** — EXPORTER_SEED env var seeds per-series
   PRNG (math/rand) deterministically so all 3 arms in a sequential
   run emit identical latency value sequences. Without it, cross-arm
   sampling noise masked DDSketch ε behavior in the accuracy report
   (b0 vs b1 differed by 0.04-0.4% just from random draws, not engine
   error). Default seed of 42 in run_demo.sh; back-compat when unset
   (auto-random, original behavior). Per-series PRNG uses
   `seed ^ hash(EXPORTER_PRODUCER_ID) ^ (seriesIdx+1)*prime` so
   distinct producers + series get distinct sequences.

2. **b0/b1 PRW exporter config — add_metric_suffixes: false** —
   VictoriaMetrics' OTLP→PRW path appends `_milliseconds` to metric
   names with `WithUnit("ms")` annotation (Prometheus naming convention).
   Asap tier preserves the original name. Without this fix, the
   accuracy comparison probe queries `http_requests_total_latency_ms`
   land on different metric names per tier — apples-to-oranges.

3. **mvp-workload.yaml** (singlenode + multinode) — dropped
   `grouping_labels: [zone]` from quantile-metric entries
   (http_requests_total_latency_ms, request_size_bytes). Per-series
   DDSketch / KLL sketches preserve PromQL's per-series semantics so
   `quantile_over_time(...)` returns comparable per-series rows in
   both asap and baseline. Counter-metric entries retain
   grouping_labels: [zone] (Sum aggregation is mergeable; no semantic
   asymmetry there). The KLL override on the latency entry is also
   tracked here for the DDSketch-vs-KLL accuracy comparison documented
   in mvp_smoke_test_findings.md.

4. **run_demo.sh** — pass EXPORTER_SEED=42 (default) to all producers.

Accuracy results post all fixes (multinode all-arms):
- p50 quantile: 0.4-0.6% rel-err vs baseline ✅
- p99 quantile: 11-12% rel-err (DDSketch ε + temporal-window variance)
- max by (zone) (quantile_over_time(...)): 8-9% rel-err (DDSketch)
- sum-by-zone/rate/topk: post-#299 returns delta-window semantics
  (not directly comparable to baseline cumulative as raw numbers, but
  bug-correct; ratio drops from ~300× to ~1× when normalized).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 19, 2026
…ativetodelta + seed) (#398)

* fix(deploy): cumulativetodelta upstream of agent routing (paired with ASAPQuery-backend #299)

Static bootstrap counterpart to ASAPQuery-backend PR #299. Without
this static fix, the asap-otel agent's BOOTSTRAP config (used before
the controller's OpAMP push lands) lacks cumulativetodelta and sends
cumulative-temporality Counter values to the backend's SumAccumulator
— triggering the ~300× per-window quadratic blowup that bug #298
documented. After OpAMP push (typed-stage-split emit from #299), the
running config has it; this static yaml just keeps the pre-OpAMP
boot window correct too.

`match_type: strict` keeps the processor a no-op for gauges
(http_requests_total_latency_ms etc. — quantile workloads unaffected).
b0/b1 baseline configs are intentionally untouched: VictoriaMetrics
expects cumulative (Prometheus convention).

Closes the ASAPCollector half of #298.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(demo): accuracy-validation harness — EXPORTER_SEED + baseline PRW config + per-series quantile

Bundle of supporting changes for apples-to-apples accuracy validation
of asap-tier vs baseline (b0/b1 VictoriaMetrics). Lands alongside the
quantile-shape + cumulativetodelta engine fixes (ASAPQuery-backend
PR #297 + #299, ASAPCollector cumulativetodelta commit above).

Changes:

1. **fake-exporter/main.go** — EXPORTER_SEED env var seeds per-series
   PRNG (math/rand) deterministically so all 3 arms in a sequential
   run emit identical latency value sequences. Without it, cross-arm
   sampling noise masked DDSketch ε behavior in the accuracy report
   (b0 vs b1 differed by 0.04-0.4% just from random draws, not engine
   error). Default seed of 42 in run_demo.sh; back-compat when unset
   (auto-random, original behavior). Per-series PRNG uses
   `seed ^ hash(EXPORTER_PRODUCER_ID) ^ (seriesIdx+1)*prime` so
   distinct producers + series get distinct sequences.

2. **b0/b1 PRW exporter config — add_metric_suffixes: false** —
   VictoriaMetrics' OTLP→PRW path appends `_milliseconds` to metric
   names with `WithUnit("ms")` annotation (Prometheus naming convention).
   Asap tier preserves the original name. Without this fix, the
   accuracy comparison probe queries `http_requests_total_latency_ms`
   land on different metric names per tier — apples-to-oranges.

3. **mvp-workload.yaml** (singlenode + multinode) — dropped
   `grouping_labels: [zone]` from quantile-metric entries
   (http_requests_total_latency_ms, request_size_bytes). Per-series
   DDSketch / KLL sketches preserve PromQL's per-series semantics so
   `quantile_over_time(...)` returns comparable per-series rows in
   both asap and baseline. Counter-metric entries retain
   grouping_labels: [zone] (Sum aggregation is mergeable; no semantic
   asymmetry there). The KLL override on the latency entry is also
   tracked here for the DDSketch-vs-KLL accuracy comparison documented
   in mvp_smoke_test_findings.md.

4. **run_demo.sh** — pass EXPORTER_SEED=42 (default) to all producers.

Accuracy results post all fixes (multinode all-arms):
- p50 quantile: 0.4-0.6% rel-err vs baseline ✅
- p99 quantile: 11-12% rel-err (DDSketch ε + temporal-window variance)
- max by (zone) (quantile_over_time(...)): 8-9% rel-err (DDSketch)
- sum-by-zone/rate/topk: post-#299 returns delta-window semantics
  (not directly comparable to baseline cumulative as raw numbers, but
  bug-correct; ratio drops from ~300× to ~1× when normalized).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

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