From 562f03894ca8bb4e1a0e700e5bad61695f4e5e35 Mon Sep 17 00:00:00 2001 From: zz_y Date: Thu, 27 Aug 2026 13:48:24 -0600 Subject: [PATCH 1/6] docs: align OpAMP delivery with plan ownership --- docs/developer_docs/opamp-config-push.md | 342 ++++++++++------------- 1 file changed, 146 insertions(+), 196 deletions(-) diff --git a/docs/developer_docs/opamp-config-push.md b/docs/developer_docs/opamp-config-push.md index 7486fe08..a6fcf8a0 100644 --- a/docs/developer_docs/opamp-config-push.md +++ b/docs/developer_docs/opamp-config-push.md @@ -1,198 +1,148 @@ -# OpAMP Config Push: Controller → Supervisor → Collector - -**Status**: implemented and working end-to-end as of [#133]() (commit `4b196e1`, "feat: OpAMP Supervisor config — push-based config from controller to collector"). This document captures the architecture, the restart semantics, and the test surface so future contributors don't have to re-derive it from the commit history. - -This is the **control plane** side of the pipeline. The data plane — sketch bytes flowing from collector processors to the ASAPQuery-backend ingest path over modified OTLP — is. - ---- - -## 1. Three-process topology - -``` -┌────────────────────┐ -│ Controller │ (Rust, long-running) -│ asap-controller │ -│ │ -│ ┌──────────────┐ │ -│ │ OpAMP Server │◀─┼── WebSocket /v1/opamp (default :4320) -│ │ (axum) │ │ -│ └──────┬───────┘ │ -│ │ push() │ -│ ▼ │ -│ replanner │ (SLA-violation loop, REST /api/v1/plan) -└────────────────────┘ - ▲ - │ persistent WebSocket - │ (agent-initiated) - │ -┌───────────┼────────────────────────────────────────────┐ -│ ▼ │ -│ ┌──────────────────┐ │ -│ │ OpAMP Supervisor │ (Go, github.com/open-telemetry/ │ -│ │ (binary) │ opentelemetry-collector/cmd/ │ -│ │ │ opampsupervisor) │ -│ └──────┬───────────┘ │ -│ │ │ -│ │ fork/exec + SIGTERM on config change │ -│ ▼ │ -│ ┌──────────────────┐ │ -│ │ asap-otel │ (the unified collector binary — │ -│ │ collector │ every sketch processor compiled │ -│ │ │ in: countmin, ddsketch, kll, │ -│ │ │ hll, countsketch, serf, gorilla) │ -│ │ ┌────────────┐ │ │ -│ │ │ opamp- │ │ │ -│ │ │ extension │ │ (reports health + config_hash │ -│ │ │ │◀─┼── back to controller on the │ -│ │ └────────────┘ │ supervisor's socket) │ -│ └──────────────────┘ │ -│ │ -│ one collector host │ -└─────────────────────────────────────────────────────────┘ -``` - -The supervisor and the collector are **two separate OS processes** on the same host. The supervisor owns the long-lived WebSocket connection to the controller and the lifecycle of the child collector. - ---- - -## 2. Push call graph - -### 2a. Controller side (Rust) - -**File**: `controller/src/opamp/mod.rs` - -| Symbol | Line | Role | -|---|---|---| -| `OpampServer` | 77–101 | Axum handler + connected-agent registry | -| `OpampServer::ws_handler` | 121–144 | HTTP `GET /v1/opamp` → WebSocket upgrade, calls `handle_socket` | -| `OpampServer::push` | 147 | Send a `RemoteConfig` to one specific agent by `agent_id` | -| `OpampServer::push_to_role` | 162 | Broadcast a `RemoteConfig` to all agents matching a role (`AgentRole::Agent`, `AgentRole::Backend`, etc.) | -| `OpampServer::push_all` | 156 | Broadcast to every connected agent | - -**Triggers** (all in `controller/src/main.rs`): - -1. **`POST /api/v1/plan`** — external HTTP entry point that accepts a new plan payload. The handler (lines 310–404) computes agent and backend configs, then calls: - - `opamp.push_to_role(AgentRole::Agent, agent_config)` — lines 337–340 - - `opamp.push_to_role(AgentRole::Backend, backend_config)` — lines 350–353 - -2. **On-connect callback** (lines 133–159) — fires when a new agent's WebSocket arrives. Looks up the agent's registered metrics, calls `replanner.push_config_to_agent(agent_id)` which pulls the current plan for those metrics and pushes it. This is how a newly-started supervisor bootstraps its first config. - -3. **Replanner** (`controller/src/replan.rs:123–178`): - - `replan_metric(metric)` — invoked when a metric's accuracy or latency SLA is violated. Recomputes the plan for that metric and pushes to every agent registered as a producer for it. - - `handle_violation(agent_id)` — the SLA-breach entry point wired to the monitoring loop. - -### 2b. Supervisor side (Go binary) - -**Bootstrap config**: `opentelemetry-collector-contrib/cmd/asap-otel-opamp/supervisor-config.yaml` - -```yaml -server: - endpoint: ws://localhost:4320/v1/opamp - tls: - insecure: true -agent: - executable: ./asap-otel # path to the collector binary - description: - non_identifying_attributes: - role: agent # consumed by controller's push_to_role() +# OpAMP collection-plan delivery + +## Purpose + +OpAMP is the delivery channel between the ASAPQuery-backend control plane and +ASAPCollector. It transports the collector portion of a versioned collection +plan and returns application status and evidence. + +OpAMP does not analyze queries, choose summaries, or decide aggregation +placement. Query-to-summary planning belongs to +[ASAPPlanner](https://github.com/ProjectASAP/ASAPPlanner), while placement and +transmission-mode decisions belong to the ASAPQuery-backend control plane. + +The authoritative behavior and ownership contract is defined in the +[ASAPCollector control-plane design](../design_docs/control-plane-design.md). +This developer document describes only the OpAMP delivery boundary. + +## End-to-end ownership + +```text +query workload + | + v +ASAPPlanner + query-to-summary planning information + | + v +ASAPQuery-backend control plane + chooses placement and transmission mode + creates one versioned plan with collector and backend portions + | | + | collector portion over OpAMP | backend portion + v v +ASAPCollector ASAPQuery-backend data plane + validates and applies validates and applies + computes/transmits summaries ingests summaries and executes queries + | | + +-------------- evidence --------------+ ``` -The supervisor: - -1. Opens a WebSocket to the controller and sends an `AgentToServer` registration message including the `non_identifying_attributes.role` value (so the controller knows whether to route `push_to_role(AgentRole::Agent, ...)` to this supervisor). -2. Waits for a `ServerToAgent` message with a populated `remote_config` field. -3. On receipt, merges the received YAML with the bootstrap `config-with-opamp.yaml`, writes the merged content to `effective.yaml` in its working directory, and sends a `SIGTERM` (or platform equivalent) to the running collector child process. -4. Forks a new collector child with the new `effective.yaml`. - -### 2c. Collector side (Go binary) - -The collector binary (e.g. `asap-otel` built from `opentelemetry-collector-contrib/cmd/asap-otel`) runs the stock `opampextension`. Its role is **reporting**, not applying — the extension sends `AgentToServer` messages with: - -- Current `config_hash` (so the controller can confirm the push landed) -- Agent health status (the supervisor's restart loop proves the new config is loadable) - -The extension does **not** hot-reload the collector pipeline. All pipeline changes happen via supervisor restart. - ---- - -## 3. Why supervisor restart, not in-process hot-reload? - -The OpenTelemetry collector's pipeline is constructed at startup from the parsed YAML — processor factories build concrete `Processor` instances into a pinned DAG, and the runtime has no graceful "swap the DAG under live traffic" primitive. A config change that adds, removes, or retypes a processor cannot be applied by mutating the running pipeline; it requires re-running the factory graph. - -Two approaches exist in principle: - -- **In-process hot-reload**: pause the pipeline, rebuild the processor graph, resume. Requires every processor to support clean shutdown and state handoff. Not supported upstream as of the current collector release. -- **Supervisor restart**: kill the child, start a new child with the new config. Loses a few seconds of in-flight traffic but requires no processor-level cooperation. - -DataCollector uses supervisor restart (option 2) because it's the only pattern that works with stock collector processors today. The commit that introduced this (`4b196e1`) confirmed the chain works end-to-end on a sketch config change, including the collector restart observably picking up a new KLL processor config. - -The downside is **in-flight loss**: any sketch window currently being assembled on the old collector is lost when the child exits. This is acceptable for Phase 1 because: - -1. Config changes are rare (SLA-driven replans, not per-query) -2. The backend's precompute engine tolerates gaps via late-data-policy fallback (see the documented late-data policy) -3. A restart is observable to the controller via the `opampextension`'s health reports, so consecutive restarts trip a circuit-breaker rather than looping - -If in-process hot-reload becomes available upstream, the supervisor layer can be removed and the `opampextension` wired to apply configs directly. The controller push API doesn't change — only the agent-side apply mechanism does. - ---- - -## 4. Sketch-type capability matching - -Commit `4b196e1`'s testing originally surfaced this as an open issue: the controller had to know which sketch processors a given collector binary supported, because pushing a KLL config to a per-sketch builder (e.g. an old `countminsketchcol` that only had countmin compiled in) would crash the restarted collector on config load. - -The cleanup that consolidated all per-sketch builder dirs into the single unified `asap-otel` binary (cleanup ) collapsed this problem: every supervisor advertising `role: agent` now runs `asap-otel`, and `asap-otel` compiles in every sketch processor. The controller's `push_to_role` only needs to ensure the YAML it pushes uses processor names the unified builder registered (`countminsketchprocessor`, `ddsketchprocessor`, `kllprocessor`, `hllprocessor`, `countsketchprocessor`, `serfprocessor`, `gorillaprocessor`, …), which it does by construction — these are the same names the controller's emit table uses when generating configs. - -If a future deployment ever ships a stripped-down collector with a subset of processors, the supervisor registration can be extended to include a `processors_available: [...]` list in `non_identifying_attributes`, and `push_to_role` can filter by that list before sending. Not needed today. - ---- - -## 5. Test surface - -### 5a. What's tested today - -**File**: `controller/src/main.rs` lines 991–1238. Three Rust integration tests that use a mock WebSocket client to decode `ServerToAgent` protobufs and assert on their contents: - -1. **`agent_receives_config_on_connect_via_workload_registry`** (992–1110) — agent connects, on-connect callback triggers `push_config_to_agent`, mock client verifies the pushed YAML matches the expected plan for the agent's registered metrics. -2. **`replan_pushes_only_to_registered_agent`** (1114–1184) — `replan_metric(metric)` is called, mock clients confirm only the producer for that metric receives the push (not unrelated agents). -3. **`generated_agent_yaml_contains_opamp_extension`** (1188–1238) — static YAML validation: `extensions.opamp.server.ws.endpoint` is set, `service.extensions` includes `"opamp"`, etc. - -All three are fast (seconds), hermetic (no real supervisor, no real collector), and run as part of `cargo test -p controller`. - -### 5b. What's NOT tested today - -- **Real supervisor binary → controller → supervisor loop**. The existing tests use mock WebSocket clients; they do not spawn `opampsupervisor` or a real collector binary. -- **End-to-end restart observability**. The assertion that the child collector actually restarts and picks up the new config was done manually in and is not part of the automated suite. - -### 5c. Proposed supervisor integration test (follow-up) - -A stand-alone integration test file `controller/tests/opamp_supervisor_integration_test.rs` marked `#[ignore]` by default (so it doesn't run in `cargo test` but can be invoked with `cargo test -- --ignored`): - -1. Start the controller HTTP + OpAMP servers on ephemeral ports -2. `exec` the `opampsupervisor` binary with a temp-dir `supervisor-config.yaml` pointing at the controller's port -3. `exec` a stub child collector that just writes its argv + config path to a known temp file and sleeps -4. `POST /api/v1/plan` with a known sketch config -5. Poll the temp file until it reflects the new config (child was re-execed) -6. Assert the new config hash matches what the controller pushed - -The `#[ignore]` gate is important because the test depends on an external binary (`opampsupervisor` from `open-telemetry/opentelemetry-collector`) being installed and on filesystem/process primitives that are flaky on some CI runners. Marking it `#[ignore]` keeps it available for manual verification without blocking the fast suite. - -**This follow-up is tracked but not shipping in the planned change** — the planned change ships the architecture doc + the control-plane-design.md correction, and the proposed test layout above. The test itself can land independently once the opampsupervisor binary is pinned in CI. - ---- - -## 6. Quick reference — file:line index - -| Thing | Path | Line | -|---|---|---| -| OpAMP server struct | `controller/src/opamp/mod.rs` | 77–101 | -| WebSocket handler | `controller/src/opamp/mod.rs` | 121–144 | -| Push unicast | `controller/src/opamp/mod.rs` | 147 | -| Push to role | `controller/src/opamp/mod.rs` | 162 | -| `ServerToAgent` remote_config build | `controller/src/opamp/mod.rs` | 256–283 | -| Plan push REST handler | `controller/src/main.rs` | 310–404 | -| On-connect callback | `controller/src/main.rs` | 133–159 | -| Replanner push | `controller/src/replan.rs` | 123–178 | -| Supervisor bootstrap config | `opentelemetry-collector-contrib/cmd/asap-otel-opamp/supervisor-config.yaml` | — | -| Collector-with-opamp config | `opentelemetry-collector-contrib/cmd/asap-otel-opamp/config-with-opamp.yaml` | — | -| Existing mock-client tests | `controller/src/main.rs` | 991–1238 | -| Original supervisor e2e commit | `git show 4b196e1` | — | +ASAPCollector never reconstructs ASAPPlanner rules from the received +configuration. It treats the plan as an execution contract. + +## OpAMP plan envelope + +Every delivered collector plan includes or unambiguously binds: + +- target collector identity; +- plan identity and monotonically ordered version; +- plan digest; +- activation and expiry conditions; +- metric selection and retained labels; +- summary family and accuracy parameters; +- aggregation and window rules; +- raw, full-summary, or delta-summary transmission mode; and +- the backend compatibility identity needed for emitted payloads. + +Defaults that affect query semantics are resolved before delivery. The +collector must not infer a missing summary parameter, grouping rule, +transmission mode, or expiry policy. + +## Delivery and application sequence + +1. The ASAPQuery-backend control plane targets a collector and sends its plan + portion through an OpAMP remote-configuration message. +2. ASAPCollector verifies the target, plan identity, version, digest, lifetime, + and every requested capability. +3. If any required field or capability is invalid, ASAPCollector rejects the + entire candidate and preserves the current valid plan. +4. If validation succeeds, ASAPCollector activates the candidate atomically + for new observations. +5. ASAPCollector reports accepted or rejected status with the plan identity, + version, digest, and reason. +6. Emitted summary payloads carry the active compatibility identity so the + ASAPQuery-backend data plane can reject state from a different plan. + +An OpAMP transport acknowledgement proves only message delivery. MVP evidence +requires a successful application report plus observations and emitted bytes +attributed to the active plan. + +## Version and retry behavior + +- Re-delivery of the same plan identity, version, and digest is idempotent. +- Reuse of an identity and version with a different digest is rejected. +- A version older than the active version is stale and rejected. +- A newer invalid version does not replace the active valid version. +- Reconnection does not reset version ordering or make an expired plan valid. +- A plan may continue during disconnection only under its declared + last-known-good lifetime and the configured maximum disconnect duration. + +These rules prevent retries and out-of-order delivery from rolling the +collector back or silently changing query semantics. + +## Application mechanism + +The contract requires atomic activation but does not require a particular +runtime mechanism. A deployment may apply a plan through safe in-process +reconfiguration or through a supervised collector replacement. Whichever +mechanism is selected must expose the same plan identity, status, transition +time, and failure evidence. + +If activation interrupts an open window, the transition must follow the plan's +declared state policy. State produced under incompatible plan versions is not +merged. + +## Failure behavior + +| Scenario | Required result | +| --- | --- | +| OpAMP message is malformed | Reject it and retain the active valid plan. | +| Requested summary is unsupported | Reject the whole candidate with an explicit capability error. | +| Plan version is stale | Reject it without changing active processing. | +| Application fails after validation | Report failure; do not claim the candidate is active. | +| Control-plane connection is lost | Continue only under the bounded last-known-good policy, then stop or enter the declared safe mode. | +| Backend plan portion is incompatible | Backend rejects emitted state; the run cannot pass MVP validation. | + +Failures are tied to the candidate plan identity and remain visible to the +end-to-end harness. + +## MVP validation scenario + +For each raw, full-summary, and supported delta-summary mode, the harness: + +1. records the plan produced by the ASAPQuery-backend control plane; +2. records the OpAMP message identity, version, and digest; +3. waits for ASAPCollector's successful application report; +4. sends observations after the recorded activation time; +5. verifies collector counters and emitted payloads reference that plan; +6. verifies the ASAPQuery-backend data plane applied the compatible backend + portion and executed the declared summary-based query; and +7. fails on missing, stale, rejected, or previous-run evidence. + +A controller response, transport acknowledgement, configuration file, or +process restart by itself is insufficient proof that the plan was applied. + +## Non-goals + +This document does not define: + +- PromQL or SQL parsing; +- query-to-summary mapping or sketch algebra; +- ASAPPlanner rewrite and optimization rules; +- ASAPQuery-backend placement or cost policy; +- summary payload encoding; or +- the runtime-specific implementation of collector reconfiguration. + +Those concerns remain owned by their respective planner, control-plane, +data-plane, and summary-design scopes. From 4ba43f73481ee6d403208e153b04e3d8bdbe2758 Mon Sep 17 00:00:00 2001 From: zz_y Date: Thu, 27 Aug 2026 14:02:19 -0600 Subject: [PATCH 2/6] docs: define OpAMP collector configuration interface --- docs/developer_docs/opamp-config-push.md | 325 ++++++++++++++--------- 1 file changed, 194 insertions(+), 131 deletions(-) diff --git a/docs/developer_docs/opamp-config-push.md b/docs/developer_docs/opamp-config-push.md index a6fcf8a0..b3459260 100644 --- a/docs/developer_docs/opamp-config-push.md +++ b/docs/developer_docs/opamp-config-push.md @@ -1,148 +1,211 @@ -# OpAMP collection-plan delivery +# OpAMP collection configuration interface ## Purpose -OpAMP is the delivery channel between the ASAPQuery-backend control plane and -ASAPCollector. It transports the collector portion of a versioned collection -plan and returns application status and evidence. - -OpAMP does not analyze queries, choose summaries, or decide aggregation -placement. Query-to-summary planning belongs to -[ASAPPlanner](https://github.com/ProjectASAP/ASAPPlanner), while placement and -transmission-mode decisions belong to the ASAPQuery-backend control plane. - -The authoritative behavior and ownership contract is defined in the -[ASAPCollector control-plane design](../design_docs/control-plane-design.md). -This developer document describes only the OpAMP delivery boundary. - -## End-to-end ownership - -```text -query workload - | - v -ASAPPlanner - query-to-summary planning information - | - v -ASAPQuery-backend control plane - chooses placement and transmission mode - creates one versioned plan with collector and backend portions - | | - | collector portion over OpAMP | backend portion - v v -ASAPCollector ASAPQuery-backend data plane - validates and applies validates and applies - computes/transmits summaries ingests summaries and executes queries - | | - +-------------- evidence --------------+ +This document defines the implemented interface used by the ASAPQuery-backend +control plane to configure ASAPCollector. Query-to-summary planning belongs to +[ASAPPlanner](https://github.com/ProjectASAP/ASAPPlanner). The +ASAPQuery-backend control plane turns that planning output into a collector +configuration and delivers it through OpAMP. + +## What is sent: protobuf containing YAML + +The current OpAMP path has two encoding layers: + +| Layer | Encoding | Content | +| --- | --- | --- | +| OpAMP message | Protocol Buffers | `ServerToAgent.remote_config` and its configuration hash | +| Collector configuration | UTF-8 YAML bytes | A complete, loadable OpenTelemetry Collector configuration | + +The configuration body is **YAML, not JSON**. JSON is used by the separate HTTP +polling control channel described below; it is not the current OpAMP payload. + +## OpAMP fields + +The ASAPQuery-backend control plane sends an OpAMP `ServerToAgent` message with +the following fields: + +| Field | Type | Definition | +| --- | --- | --- | +| `remote_config` | `AgentRemoteConfig` | Remote configuration addressed to the connected collector agent. | +| `remote_config.config` | `AgentConfigMap` | Map of named configuration files. | +| `remote_config.config.config_map[""]` | `AgentConfigFile` | Preferred single-file entry containing the collector configuration. | +| `remote_config.config.config_map["asap-otel"]` | `AgentConfigFile` | ASAP-named entry, used when the empty-key entry is absent or empty. | +| `...body` | `bytes` | UTF-8 bytes containing the complete collector YAML. | +| `remote_config.config_hash` | `bytes` | Opaque identity of this exact remote configuration, generated by the control plane. | + +The collector currently also accepts the first non-empty configuration-map +entry when neither conventional entry contains a body. Senders should use the +empty key for a single-file OpAMP configuration or `"asap-otel"` for the ASAP +named-entry convention; fallback selection should not be relied on as part of +the interface. + +Because the remote body replaces the collector's configuration file, it must +include receivers, processors, exporters, and service pipelines. Sending only +an `asap_edge` processor fragment produces an incomplete collector +configuration. + +## ASAP processor YAML fields + +The MVP-relevant configuration is under `processors.`, where the +example below uses the component ID `asap_edge`. + +| Field | Type | Definition | +| --- | --- | --- | +| `edge_id` | string | Stable identity of the collector producing summaries. | +| `shard_count` | integer | Number of collector processing shards. | +| `window_duration` | duration | Duration of each summary window, for example `1m`. | +| `warm_allowed_lateness` | duration | How long late observations may update a window. | +| `drop_original` | boolean | If true, transmit summaries without forwarding the original raw observations. | +| `delta_transmission` | boolean | Default transmission mode for metric entries that do not override it. | +| `metrics` | list | Per-metric summary instructions. | +| `metrics[].metric` | string | Input metric name to match. | +| `metrics[].family` | enum | Summary family: `sum`, `ddsketch`, `kll`, `hll`, `countsketch`, or `countminsketch`. | +| `metrics[].mode` | enum | `per_series` keeps separate input series; `whole_stream` combines matching series before grouping. | +| `metrics[].aggregate_by` | list of strings | Labels retained as output grouping keys. Labels not listed here are aggregated away. | +| `metrics[].relative_accuracy` | number | DDSketch relative-error parameter. Required when DDSketch semantics depend on it. | +| `metrics[].k` | integer | KLL capacity/accuracy parameter. | +| `metrics[].rows`, `metrics[].cols` | integers | CountSketch or Count-Min Sketch dimensions. | +| `metrics[].sample_p` | number | Sampling probability when sampling is selected. | +| `metrics[].max_series` | integer | Per-metric series-cardinality limit. | +| `metrics[].delta_transmission` | boolean | Per-metric override selecting delta (`true`) or full-state (`false`) transmission. | +| `metrics[].delta_threshold` | number | Minimum accumulated change that triggers an eligible delta emission. | +| `metrics[].item_label` | string | Label interpreted as the item for set/frequency summaries. | + +Family-specific parameters are required only for the selected family. The +control plane must resolve semantics-changing defaults before delivery instead +of depending on collector-side guesses. Delta mode may be selected only for a +family whose collector and backend implementations support compatible delta +semantics; KLL is full-state-only in the current design. + +### Complete example + +This example asks the collector to produce one DDSketch per `(service, region)` +group and transmit deltas: + +```yaml +receivers: + otlp: + protocols: + grpc: {} + +processors: + asap_edge: + edge_id: edge-a + shard_count: 4 + window_duration: 1m + warm_allowed_lateness: 10s + drop_original: true + metrics: + - metric: request_duration_seconds + family: ddsketch + mode: per_series + aggregate_by: [service, region] + relative_accuracy: 0.01 + delta_transmission: true + delta_threshold: 1 + +exporters: + otlp: + endpoint: asapquery-backend:4317 + tls: + insecure: true + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [asap_edge] + exporters: [otlp] ``` -ASAPCollector never reconstructs ASAPPlanner rules from the received -configuration. It treats the plan as an execution contract. - -## OpAMP plan envelope - -Every delivered collector plan includes or unambiguously binds: - -- target collector identity; -- plan identity and monotonically ordered version; -- plan digest; -- activation and expiry conditions; -- metric selection and retained labels; -- summary family and accuracy parameters; -- aggregation and window rules; -- raw, full-summary, or delta-summary transmission mode; and -- the backend compatibility identity needed for emitted payloads. - -Defaults that affect query semantics are resolved before delivery. The -collector must not infer a missing summary parameter, grouping rule, -transmission mode, or expiry policy. - -## Delivery and application sequence - -1. The ASAPQuery-backend control plane targets a collector and sends its plan - portion through an OpAMP remote-configuration message. -2. ASAPCollector verifies the target, plan identity, version, digest, lifetime, - and every requested capability. -3. If any required field or capability is invalid, ASAPCollector rejects the - entire candidate and preserves the current valid plan. -4. If validation succeeds, ASAPCollector activates the candidate atomically - for new observations. -5. ASAPCollector reports accepted or rejected status with the plan identity, - version, digest, and reason. -6. Emitted summary payloads carry the active compatibility identity so the - ASAPQuery-backend data plane can reject state from a different plan. - -An OpAMP transport acknowledgement proves only message delivery. MVP evidence -requires a successful application report plus observations and emitted bytes -attributed to the active plan. - -## Version and retry behavior - -- Re-delivery of the same plan identity, version, and digest is idempotent. -- Reuse of an identity and version with a different digest is rejected. -- A version older than the active version is stale and rejected. -- A newer invalid version does not replace the active valid version. -- Reconnection does not reset version ordering or make an expired plan valid. -- A plan may continue during disconnection only under its declared - last-known-good lifetime and the configured maximum disconnect duration. - -These rules prevent retries and out-of-order delivery from rolling the -collector back or silently changing query semantics. - -## Application mechanism - -The contract requires atomic activation but does not require a particular -runtime mechanism. A deployment may apply a plan through safe in-process -reconfiguration or through a supervised collector replacement. Whichever -mechanism is selected must expose the same plan identity, status, transition -time, and failure evidence. - -If activation interrupts an open window, the transition must follow the plan's -declared state policy. State produced under incompatible plan versions is not -merged. +Given raw series distinguished by `service`, `region`, `instance`, and `pod`, +this configuration retains `service` and `region` and aggregates away +`instance` and `pod`. It is intended to support a query such as: -## Failure behavior +```promql +quantile_over_time(0.95, request_duration_seconds[5m]) +``` + +The precise PromQL-to-summary choice is supplied by ASAPPlanner and the +ASAPQuery-backend control plane; the collector only applies the delivered +execution configuration. + +## Apply behavior and response + +On receipt, ASAPCollector: -| Scenario | Required result | +1. selects the empty-key entry, then `"asap-otel"`, then another non-empty + entry as a compatibility fallback; +2. parses its body as YAML; +3. writes the resulting complete YAML to the configured remote-config path; +4. reports the remote-config status; and +5. exits when a changed configuration requires the supervisor to restart it. + +If the received YAML is identical to the configuration already on disk, the +collector reports it as applied without requesting another restart. + +The collector response contains: + +| Field | Definition | | --- | --- | -| OpAMP message is malformed | Reject it and retain the active valid plan. | -| Requested summary is unsupported | Reject the whole candidate with an explicit capability error. | -| Plan version is stale | Reject it without changing active processing. | -| Application fails after validation | Report failure; do not claim the candidate is active. | -| Control-plane connection is lost | Continue only under the bounded last-known-good policy, then stop or enter the declared safe mode. | -| Backend plan portion is incompatible | Backend rejects emitted state; the run cannot pass MVP validation. | +| `last_remote_config_hash` | The `config_hash` of the configuration being reported. | +| `status` | `APPLIED` when the YAML was accepted and written (or was already identical), otherwise `FAILED`. | +| `error_message` | Parsing or file-application error when status is `FAILED`. | -Failures are tied to the candidate plan identity and remain visible to the -end-to-end harness. +`APPLIED` does not by itself prove that the restarted collector loaded the +configuration, processed observations, or emitted the requested summaries. +The MVP harness must additionally capture collector startup, active processor +configuration, processing counters, emitted payloads, and backend query +evidence. -## MVP validation scenario +## JSON HTTP polling is a different interface -For each raw, full-summary, and supported delta-summary mode, the harness: +The repository also has an HTTP polling control channel. It returns a JSON +`PrecomputeConfigSet`, whose current top-level Go field names are: -1. records the plan produced by the ASAPQuery-backend control plane; -2. records the OpAMP message identity, version, and digest; -3. waits for ASAPCollector's successful application report; -4. sends observations after the recorded activation time; -5. verifies collector counters and emitted payloads reference that plan; -6. verifies the ASAPQuery-backend data plane applied the compatible backend - portion and executed the declared summary-based query; and -7. fails on missing, stale, rejected, or previous-run evidence. +| JSON field | Definition | +| --- | --- | +| `Version` | Monotonically increasing HTTP plan version. | +| `Configs` | List of runtime precompute configurations. | -A controller response, transport acknowledgement, configuration file, or -process restart by itself is insufficient proof that the plan was applied. +Its acknowledgement body is: -## Non-goals +```json +{"plan_version": 42} +``` + +This JSON contract is not embedded in the OpAMP YAML body. The runtime +`OpAmpChannel` implementation is currently a stub, so documentation and tests +must not claim that `PrecomputeConfigSet` is delivered through OpAMP today. + +## Current contract gap -This document does not define: +The design requires one compatible plan for the collector and ASAPQuery data +plane, but the implemented OpAMP YAML schema currently has no explicit +`plan_id`, `plan_version`, activation time, expiry time, or backend +compatibility identifier. `config_hash` identifies the remote collector +configuration; it is not a complete versioned end-to-end plan contract. -- PromQL or SQL parsing; -- query-to-summary mapping or sketch algebra; -- ASAPPlanner rewrite and optimization rules; -- ASAPQuery-backend placement or cost policy; -- summary payload encoding; or -- the runtime-specific implementation of collector reconfiguration. +Until those fields are implemented and validated, the MVP harness must record +the OpAMP `config_hash` and independently prove which backend plan was active. +It must not infer plan ordering, expiry, or collector/backend compatibility +from `APPLIED` alone. + +## Failure behavior + +| Scenario | Current required result | +| --- | --- | +| Missing configuration map or non-empty body | Log and ignore the message. Because the current code sends no failure status, the MVP harness must fail on its missing application evidence. | +| Body is not valid YAML | Report `FAILED`; do not write it as the active file. | +| Remote-config file cannot be written | Report `FAILED` with the error. | +| Changed YAML is written | Report `APPLIED`; supervisor restart is still required. | +| Collector fails after restart | Treat the MVP scenario as failed even if OpAMP previously reported `APPLIED`. | +| Backend applies an incompatible plan | Reject the emitted state or fail the run; never return a plausible mismatched result. | + +## Non-goals -Those concerns remain owned by their respective planner, control-plane, -data-plane, and summary-design scopes. +This document does not define PromQL parsing, query-to-summary algebra, +ASAPPlanner optimization rules, summary payload encoding, or +ASAPQuery-backend query execution. It defines only the control-plane-to- +collector configuration boundary and the evidence needed to validate it. From 5afc38bdf4072b611f7097f406ee3f6aeab09272 Mon Sep 17 00:00:00 2001 From: zz_y Date: Thu, 27 Aug 2026 14:24:43 -0600 Subject: [PATCH 3/6] docs: point the OpAMP interface at ASAPQuery-backend's compiled-plan redesign ASAPPlanner's post-ASAP IR (crates/types/src/post_asap) selects a candidate logical DAG (SummaryAgg/summary family+algorithm+params/ Reduction/SummaryEstimate) and, per its own README "Scope" and asap-aware-mapping/README.md "Non-Goals", explicitly does not choose collector/backend placement, transport mode, or physical resources. Note in Purpose that the ASAPQuery-backend control plane compiles that selection - it does not serialize it directly - into this document's collector configuration and a companion backend configuration sharing one plan identity, and link ASAPQuery-backend PR #444's new design-compiled-plan-collector-backend-split.md for that compile step. Point the existing "Current contract gap" section (no plan_id/version/ activation/expiry/backend-compat on this wire) at that same design as the proposed closing mechanism, without claiming any of it is implemented here yet - the OpAMP envelope encoding for those fields is still an open question in that design, to be resolved and documented in this file once it lands. Co-Authored-By: Claude Sonnet 5 --- docs/developer_docs/opamp-config-push.md | 29 +++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/developer_docs/opamp-config-push.md b/docs/developer_docs/opamp-config-push.md index b3459260..d61d513b 100644 --- a/docs/developer_docs/opamp-config-push.md +++ b/docs/developer_docs/opamp-config-push.md @@ -4,9 +4,19 @@ This document defines the implemented interface used by the ASAPQuery-backend control plane to configure ASAPCollector. Query-to-summary planning belongs to -[ASAPPlanner](https://github.com/ProjectASAP/ASAPPlanner). The -ASAPQuery-backend control plane turns that planning output into a collector -configuration and delivers it through OpAMP. +[ASAPPlanner](https://github.com/ProjectASAP/ASAPPlanner), which selects a +candidate post-ASAP logical DAG (`SummaryAgg`/summary family, algorithm, and +parameters/`Reduction`/`SummaryEstimate` — see its +`crates/types/src/post_asap` module) and explicitly does not choose +collector/backend placement, transport mode, or physical resources (its own +README "Scope" and `asap-aware-mapping/README.md` "Non-Goals"). The +ASAPQuery-backend control plane compiles that selected DAG — it does not +serialize it directly — into this collector configuration and a companion +backend configuration that share one plan identity; see +[ASAPQuery-backend's compiled-plan design](https://github.com/ProjectASAP/ASAPQuery-backend/blob/docs/asapplanner-workload-planner-migration/control_plane/docs/design-compiled-plan-collector-backend-split.md) +(proposed, [PR #444](https://github.com/ProjectASAP/ASAPQuery-backend/pull/444)) +for that compile step, and the "Current contract gap" section below for what +of it is not implemented on this wire yet. ## What is sent: protobuf containing YAML @@ -192,6 +202,19 @@ the OpAMP `config_hash` and independently prove which backend plan was active. It must not infer plan ordering, expiry, or collector/backend compatibility from `APPLIED` alone. +ASAPQuery-backend's compiled-plan design (linked above) proposes closing this +gap by having the control plane's compile step stamp `plan_id`/ +`plan_version`/`activation`/`expiry`/`backend_compat` identically onto both +the collector configuration this document describes and the companion +`BackendPlan` it compiles alongside it, so the two can be checked for +agreement directly instead of only through `config_hash`. That design does +not yet specify where in this document's OpAMP envelope those fields should +live — a sibling key next to `processors.asap_edge`, a field inside +`asap_edge` itself, or a separate `AgentConfigFile` entry are all still +open — and none of them are implemented on this wire today. This section +should be updated to document the chosen encoding once that decision is +made and implemented here, not before. + ## Failure behavior | Scenario | Current required result | From 8780ac107433ad87fc2adb14721c18110b1cefc3 Mon Sep 17 00:00:00 2001 From: zz_y Date: Thu, 27 Aug 2026 15:18:11 -0600 Subject: [PATCH 4/6] docs: point asap_edge field table at the DAG-aligned schema redesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a 'Proposed: align these fields with the post-ASAP DAG's own vocabulary' subsection after the current field table and example, linking ASAPQuery-backend's design-compiled-plan-collector-backend- split.md §5. That design is checked against the real asapedgeprocessor/config.go (mode is a bare per_series/whole_stream string, aggregate_by has no GroupKeys.without equivalent, family has no exact_kind or GroupingStrategy slot) and proposes renaming/extending metrics[] to name every field directly from post_asap while keeping Go's flat mapstructure-struct idiom - not a nested tagged union. The existing field table and example are left as-is: they correctly document what config.go implements today, and this addition does not change that. Co-Authored-By: Claude Sonnet 5 --- docs/developer_docs/opamp-config-push.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/developer_docs/opamp-config-push.md b/docs/developer_docs/opamp-config-push.md index d61d513b..2ff75623 100644 --- a/docs/developer_docs/opamp-config-push.md +++ b/docs/developer_docs/opamp-config-push.md @@ -141,6 +141,29 @@ The precise PromQL-to-summary choice is supplied by ASAPPlanner and the ASAPQuery-backend control plane; the collector only applies the delivered execution configuration. +### Proposed: align these fields with the post-ASAP DAG's own vocabulary + +**Not implemented.** The table above matches +`opentelemetry-collector-contrib-patch/processor/asapedgeprocessor/config.go` +today. That struct predates ASAPPlanner's current `post_asap` IR and +reinvents two vocabularies instead of naming it directly: `mode` is a bare +string with exactly two values (`per_series`/`whole_stream`) and +`aggregate_by` is a plain string list — neither can distinguish +`Reduction::PerEntity` from a genuine zero-key `Reduce`, and `family` has no +discriminator for the four non-`Sum` exact kinds or any field for +`GroupingStrategy` (Hydra). ASAPQuery-backend's compiled-plan design +proposes renaming/extending `metrics[]` so every field and enum value is +spelled directly from `post_asap` — +`source`/`family`/`exact_kind`/`reduce_by`/`reduce_without`/`per_entity`/ +`grouping`/`hydra_kind`/`shared_rows`/`shared_columns` — as a flat, +`mapstructure`-shaped struct (matching this same file's existing +`FamilyKind`/`Tier`/`ColdFormat` pattern), not a nested tagged union. See +[§5 of that design](https://github.com/ProjectASAP/ASAPQuery-backend/blob/docs/asapplanner-workload-planner-migration/control_plane/docs/design-compiled-plan-collector-backend-split.md#5-redesigning-asap_edgemetrics-to-match-the-dags-own-shape) +for the full field-by-field redesign and a worked example. This section +should be rewritten against the table above once that redesign lands in +`config.go`, with old field names accepted as deprecated aliases for one +release. + ## Apply behavior and response On receipt, ASAPCollector: From f459abf5d626d0c923c1a8b1b464deae89762b1e Mon Sep 17 00:00:00 2001 From: zz_y Date: Thu, 27 Aug 2026 15:37:53 -0600 Subject: [PATCH 5/6] docs: redesign collector plan interface around ASAPPlanner --- docs/developer_docs/opamp-config-push.md | 590 ++++++++++++++--------- 1 file changed, 360 insertions(+), 230 deletions(-) diff --git a/docs/developer_docs/opamp-config-push.md b/docs/developer_docs/opamp-config-push.md index 2ff75623..ce6fe8e2 100644 --- a/docs/developer_docs/opamp-config-push.md +++ b/docs/developer_docs/opamp-config-push.md @@ -1,257 +1,387 @@ -# OpAMP collection configuration interface - -## Purpose - -This document defines the implemented interface used by the ASAPQuery-backend -control plane to configure ASAPCollector. Query-to-summary planning belongs to -[ASAPPlanner](https://github.com/ProjectASAP/ASAPPlanner), which selects a -candidate post-ASAP logical DAG (`SummaryAgg`/summary family, algorithm, and -parameters/`Reduction`/`SummaryEstimate` — see its -`crates/types/src/post_asap` module) and explicitly does not choose -collector/backend placement, transport mode, or physical resources (its own -README "Scope" and `asap-aware-mapping/README.md` "Non-Goals"). The -ASAPQuery-backend control plane compiles that selected DAG — it does not -serialize it directly — into this collector configuration and a companion -backend configuration that share one plan identity; see -[ASAPQuery-backend's compiled-plan design](https://github.com/ProjectASAP/ASAPQuery-backend/blob/docs/asapplanner-workload-planner-migration/control_plane/docs/design-compiled-plan-collector-backend-split.md) -(proposed, [PR #444](https://github.com/ProjectASAP/ASAPQuery-backend/pull/444)) -for that compile step, and the "Current contract gap" section below for what -of it is not implemented on this wire yet. - -## What is sent: protobuf containing YAML - -The current OpAMP path has two encoding layers: - -| Layer | Encoding | Content | +# ASAPQuery-to-ASAPCollector collection-plan interface + +## TL;DR + +ASAPPlanner produces candidate **logical** +[post-ASAP DAGs](https://github.com/ProjectASAP/ASAPPlanner/blob/main/docs/design_docs/post-asap-ir.md). +It chooses summary +semantics, such as `SummaryAgg`, a summary family and parameters, a reduction, +and `SummaryEstimate`; it does not choose where operators run or how state is +transmitted. + +The ASAPQuery-backend control plane selects a candidate and compiles it into a +physical plan bundle: + +```text +ASAPPlanner candidate post-ASAP DAG + | + v +ASAPQuery-backend control plane + candidate selection + stage allocation + runtime policy + / \ + v v +CollectorPlan BackendPlan +build/transmit materializations ingest/route/read materializations + \ / + +-- same plan and materialization identities --+ +``` + +The collector portion is sent in an OpAMP protobuf `AgentRemoteConfig`. Its +body is a versioned YAML `CollectorPlan`, not a serialized ASAPPlanner DAG and +not an untyped map of processor options. The collector validates and applies +the whole plan atomically and reports the active plan identity. + +This document defines the target interface. The final section distinguishes it +from the narrower interface implemented today. + +## Ownership boundary + +| Component | Owns | Must not own | | --- | --- | --- | -| OpAMP message | Protocol Buffers | `ServerToAgent.remote_config` and its configuration hash | -| Collector configuration | UTF-8 YAML bytes | A complete, loadable OpenTelemetry Collector configuration | +| ASAPPlanner | Query parsing and canonicalization; candidate post-ASAP DAGs; summary family, algorithm, parameters, reduction, and readout semantics | Collector/backend placement, runtime resources, transmission mode, OpAMP encoding | +| ASAPQuery-backend control plane | Candidate selection; physical placement; source binding; window materialization; transmission policy; plan versioning; splitting one decision into collector and backend portions | Reimplementing PromQL-to-summary rules already represented by ASAPPlanner | +| ASAPCollector | Validating and executing `CollectorPlan`; building and transmitting the named materializations | Selecting a different summary, changing parameters, or inferring omitted query semantics | +| ASAPQuery-backend data plane | Installing the matching `BackendPlan`; ingesting materializations; applying `SummaryEstimate` and remaining logical operators | Re-planning a different summary at query time | -The configuration body is **YAML, not JSON**. JSON is used by the separate HTTP -polling control channel described below; it is not the current OpAMP payload. +ASAPPlanner's documented +[scope](https://github.com/ProjectASAP/ASAPPlanner#scope) excludes stage and +physical-resource assignment. It also describes the downstream split as an +open integration question: one post-ASAP plan must become a streaming graph +that constructs summaries and a query plan that reads them. This interface +places that split in the ASAPQuery-backend physical-planning layer. -## OpAMP fields +## From ASAPPlanner DAG to the two runtime plans -The ASAPQuery-backend control plane sends an OpAMP `ServerToAgent` message with -the following fields: +The control plane performs the following compilation, without changing the +selected candidate's semantics: -| Field | Type | Definition | +| ASAPPlanner concept | Collector plan | Backend plan | +| --- | --- | --- | +| `SummaryAgg` | A `materialization` producer | A materialization declaration and storage route | +| `SummaryFamilyType` | `summary.family`, `summary.algorithm`, and typed `summary.parameters` | The identical family, algorithm, and parameters | +| `col` | `input.value` or `input.item_label` | Readout input metadata | +| `Reduction::PerEntity` | `reduction.kind: per_entity` | Preserve each source series identity | +| `Reduction::Reduce(GroupKeys)` | `reduction.kind: reduce` plus explicit `by` or `without` labels | Matching group/roll-up shape | +| Time-range semantics around `SummaryAgg` | Concrete streaming `window` and lateness policy | Compatible query-window/read range | +| `SummaryEstimate` | No collector readout; collector emits summary state | Query-time readout, such as quantile, cardinality, point count, or top-k | +| `Logical` subtree | Raw pass-through only when the physical plan explicitly selects it | Exact execution or configured fallback | +| `SummaryMerge` | Shard/stage outputs with the same materialization contract | Merge only states with identical compatibility fields | + +A control-plane-assigned reference to a Planner node is useful for traceability +but is not a runtime identity. The physical control plane creates stable, +content-addressed materialization identities after source binding, placement, +windowing, and transmission have been decided. + +## Wire encoding + +The interface has two layers: + +| Layer | Encoding | Contract | | --- | --- | --- | -| `remote_config` | `AgentRemoteConfig` | Remote configuration addressed to the connected collector agent. | -| `remote_config.config` | `AgentConfigMap` | Map of named configuration files. | -| `remote_config.config.config_map[""]` | `AgentConfigFile` | Preferred single-file entry containing the collector configuration. | -| `remote_config.config.config_map["asap-otel"]` | `AgentConfigFile` | ASAP-named entry, used when the empty-key entry is absent or empty. | -| `...body` | `bytes` | UTF-8 bytes containing the complete collector YAML. | -| `remote_config.config_hash` | `bytes` | Opaque identity of this exact remote configuration, generated by the control plane. | - -The collector currently also accepts the first non-empty configuration-map -entry when neither conventional entry contains a body. Senders should use the -empty key for a single-file OpAMP configuration or `"asap-otel"` for the ASAP -named-entry convention; fallback selection should not be relied on as part of -the interface. - -Because the remote body replaces the collector's configuration file, it must -include receivers, processors, exporters, and service pipelines. Sending only -an `asap_edge` processor fragment produces an incomplete collector -configuration. - -## ASAP processor YAML fields - -The MVP-relevant configuration is under `processors.`, where the -example below uses the component ID `asap_edge`. - -| Field | Type | Definition | +| Transport | OpAMP Protocol Buffers | Delivery, targeting, retry, and remote-config hash | +| `CollectorPlan` body | UTF-8 YAML with `content_type: application/yaml` | Versioned ASAP collector execution contract | + +The OpAMP `AgentConfigMap` entry is named `asap-collector-plan.yaml`. A receiver +must select this exact entry; it must not silently choose an arbitrary first +file. `AgentRemoteConfig.config_hash` is the hash used by OpAMP delivery. It is +separate from `metadata.content_hash`, which identifies the canonical plan +content across transports and processes. + +The plan is YAML because it is an operator-visible configuration artifact and +fits OpAMP's configuration-file model. YAML is only the serialization: fields +below form a closed, versioned schema. Unknown required fields, unknown enum +values, and type mismatches are validation failures. JSON may be supported in +a later schema version, but a sender must identify the media type and a +receiver must never guess it. + +## `CollectorPlan` schema + +### Envelope + +| Field | Type | Required | Definition | +| --- | --- | --- | --- | +| `api_version` | string | yes | Schema version. MVP value: `asap.io/v1alpha1`. | +| `kind` | string | yes | Must be `CollectorPlan`. | +| `metadata.plan_id` | string | yes | Identity shared by this collector plan and its matching backend plan. | +| `metadata.revision` | uint64 | yes | Monotonically increasing revision for this target. | +| `metadata.content_hash` | string | yes | SHA-256 of the canonical plan with this field omitted. | +| `metadata.generated_at` | RFC 3339 timestamp | yes | Time the control plane produced this revision. | +| `metadata.valid_from` | RFC 3339 timestamp | yes | Earliest activation time. | +| `metadata.expires_at` | RFC 3339 timestamp | yes | Time after which the collector must stop using the plan. | +| `metadata.planner_revision` | string | yes | ASAPPlanner commit/version used to create the candidate DAG. | +| `metadata.candidate_id` | string | yes | Stable identifier of the selected candidate post-ASAP plan. | +| `metadata.query_ids` | list of strings | yes | Workload queries whose selected plan requires these materializations. | +| `target.instance_uid` | string | yes | Exact OpAMP agent instance this plan targets. | +| `target.capability_hash` | string | yes | Capability snapshot against which the physical plan was validated. | +| `on_unsupported` | enum | yes | `reject_plan` for MVP. No silent downgrade or substitution is allowed. | +| `materializations` | list | yes | Collector-side producers. An empty list is valid only for an explicit no-op plan. | + +`plan_id` groups compatible collector and backend portions. `revision` orders +updates. `content_hash` makes a revision immutable. Reusing the same +`(plan_id, revision)` with different content is an error. + +### Materialization identity and input + +| Field | Type | Required | Definition | +| --- | --- | --- | --- | +| `materializations[].id` | string | yes | Content-addressed identity shared with the backend `Materialization.fingerprint`. | +| `materializations[].logical_node_ref` | string | yes | Deterministic reference assigned by the control plane to the selected Planner `SummaryAgg` or logical pass-through node. | +| `materializations[].input.metric` | string | yes | Exact input metric name. | +| `materializations[].input.matchers` | list | yes | Canonical label matchers; each has `label`, `op`, and `value`. Empty means all series of the metric. | +| `materializations[].input.value` | enum | yes | `sample_value` for numeric summaries, or `label` with `label_name` for item/set summaries. | + +Matcher `op` is one of `eq`, `neq`, `regex`, or `not_regex`. The collector +must apply the same matcher semantics used when ASAPPlanner canonicalized the +query. A metric-name regex or unresolved source is outside the MVP and must be +rejected during physical planning. + +### Summary + +| Field | Type | Required | Definition | +| --- | --- | --- | --- | +| `summary.family` | enum | yes | `exact_aggregate`, `sketch`, `sample`, `wavelet`, or `stat_model`, matching Planner `SummaryFamilyType`. | +| `summary.algorithm` | enum | yes | Concrete algorithm within the family. | +| `summary.parameters` | tagged object | yes | Parameters belonging to exactly that algorithm. Empty object for parameterless exact accumulators. | +| `summary.accuracy` | tagged object | yes | Original Planner constraint: `exact`, `epsilon`, or `epsilon_delta`. | + +When a Planner exact aggregation carries no explicit accuracy field, the +physical control plane normalizes this field to `{kind: exact}`. + +The MVP algorithms and parameter objects are: + +| Family | Algorithm | Parameters | | --- | --- | --- | -| `edge_id` | string | Stable identity of the collector producing summaries. | -| `shard_count` | integer | Number of collector processing shards. | -| `window_duration` | duration | Duration of each summary window, for example `1m`. | -| `warm_allowed_lateness` | duration | How long late observations may update a window. | -| `drop_original` | boolean | If true, transmit summaries without forwarding the original raw observations. | -| `delta_transmission` | boolean | Default transmission mode for metric entries that do not override it. | -| `metrics` | list | Per-metric summary instructions. | -| `metrics[].metric` | string | Input metric name to match. | -| `metrics[].family` | enum | Summary family: `sum`, `ddsketch`, `kll`, `hll`, `countsketch`, or `countminsketch`. | -| `metrics[].mode` | enum | `per_series` keeps separate input series; `whole_stream` combines matching series before grouping. | -| `metrics[].aggregate_by` | list of strings | Labels retained as output grouping keys. Labels not listed here are aggregated away. | -| `metrics[].relative_accuracy` | number | DDSketch relative-error parameter. Required when DDSketch semantics depend on it. | -| `metrics[].k` | integer | KLL capacity/accuracy parameter. | -| `metrics[].rows`, `metrics[].cols` | integers | CountSketch or Count-Min Sketch dimensions. | -| `metrics[].sample_p` | number | Sampling probability when sampling is selected. | -| `metrics[].max_series` | integer | Per-metric series-cardinality limit. | -| `metrics[].delta_transmission` | boolean | Per-metric override selecting delta (`true`) or full-state (`false`) transmission. | -| `metrics[].delta_threshold` | number | Minimum accumulated change that triggers an eligible delta emission. | -| `metrics[].item_label` | string | Label interpreted as the item for set/frequency summaries. | - -Family-specific parameters are required only for the selected family. The -control plane must resolve semantics-changing defaults before delivery instead -of depending on collector-side guesses. Delta mode may be selected only for a -family whose collector and backend implementations support compatible delta -semantics; KLL is full-state-only in the current design. - -### Complete example - -This example asks the collector to produce one DDSketch per `(service, region)` -group and transmit deltas: +| `exact_aggregate` | `sum`, `count`, `min_max`, `increase`, `rate` | `{}` | +| `sketch` | `kll` | `k` | +| `sketch` | `ddsketch` | `alpha` | +| `sketch` | `hll` | `precision` | +| `sketch` | `cms` | `width`, `depth` | +| `sketch` | `cms_with_heap` | `width`, `depth`, `heap_size` | +| `sketch` | `count_sketch` | `width`, `depth` | +| `sketch` | `count_sketch_with_heap` | `width`, `depth`, `heap_size` | + +ASAPPlanner also defines KMV, Theta, reservoir sampling, Haar wavelets, and +statistical models. They remain valid Planner alternatives but are rejected by +this interface until both ASAPCollector and ASAPQuery-backend advertise and +implement matching wire/state capabilities. The control plane must not map an +unsupported algorithm to a vaguely similar supported algorithm. + +`accuracy` records the constraint that justified the selected candidate; it +does not replace the concrete parameters. For example: ```yaml -receivers: - otlp: - protocols: - grpc: {} - -processors: - asap_edge: - edge_id: edge-a - shard_count: 4 - window_duration: 1m - warm_allowed_lateness: 10s - drop_original: true - metrics: - - metric: request_duration_seconds - family: ddsketch - mode: per_series - aggregate_by: [service, region] - relative_accuracy: 0.01 - delta_transmission: true - delta_threshold: 1 - -exporters: - otlp: - endpoint: asapquery-backend:4317 - tls: - insecure: true - -service: - pipelines: - metrics: - receivers: [otlp] - processors: [asap_edge] - exporters: [otlp] +summary: + family: sketch + algorithm: ddsketch + parameters: + alpha: 0.01 + accuracy: + kind: epsilon + epsilon: 0.01 ``` -Given raw series distinguished by `service`, `region`, `instance`, and `pod`, -this configuration retains `service` and `region` and aggregates away -`instance` and `pod`. It is intended to support a query such as: +The control plane must validate the algorithm/parameter pair against the +selected Planner `SketchKind`. A DDSketch with KLL's `k` parameter is invalid. + +### Reduction and windows + +| Field | Type | Required | Definition | +| --- | --- | --- | --- | +| `reduction.kind` | enum | yes | `per_entity` or `reduce`; preserves Planner's distinction. | +| `reduction.by` | list of strings | for `reduce` | Labels retained when `without` is false. An empty list means a real global reduction. | +| `reduction.without` | boolean | for `reduce` | When true, `by` names excluded labels and all other labels are retained. | +| `window.kind` | enum | yes | `tumbling` for the MVP. | +| `window.size` | duration | yes | Logical summary window. | +| `window.slide` | duration | yes | Window start interval; equal to `size` for tumbling windows. | +| `window.allowed_lateness` | duration | yes | Maximum late-arrival update interval. | + +`per_entity` is not encoded as `reduce` with an empty `by` list. The former +keeps one result per input series; the latter merges every matching series +into one global group. This distinction comes directly from ASAPPlanner's +`Reduction` type and must survive physical lowering. + +Planner time-range semantics describe what the query means. The physical +control plane selects a concrete streaming window representation capable of +answering that range. If the chosen windows cannot compose to the Planner +query's range without violating semantics or accuracy, the candidate cannot +be deployed. + +### Placement output and transmission + +| Field | Type | Required | Definition | +| --- | --- | --- | --- | +| `placement.stage` | enum | yes | Must be `collector` in this document. | +| `placement.shards` | uint32 | yes | Number of local producer shards. Physical policy, not a Planner field. | +| `transmission.mode` | enum | yes | `raw`, `full`, or `delta`. | +| `transmission.encoding` | string | yes | State encoding understood by both collector and backend. | +| `transmission.schema_version` | uint32 | yes | Version of the emitted materialization payload. | +| `transmission.emit_every` | duration | yes | Full or delta emission cadence within/following a window. | +| `transmission.full_checkpoint_every` | duration | for `delta` | Maximum interval between full checkpoints used to recover delta state. | +| `transmission.sequence_scope` | enum | for `delta` | MVP value `materialization_window_producer`. | +| `output.endpoint_ref` | string | yes | Reference to a preconfigured exporter endpoint; no credentials are embedded in the plan. | + +Transmission is a physical decision made by ASAPQuery-backend, not by +ASAPPlanner. `full` and `delta` change representation, not logical query +semantics. Delta is legal only when the advertised algorithm/state encoding +supports it. Each delta payload must carry plan ID, revision, materialization +ID, window identity, producer identity, sequence number, and base/checkpoint +identity so the backend can reject gaps or incompatible state. + +`raw` means explicit pass-through selected for a logical subtree that is not +materialized at the collector. It must not be represented as an unknown +summary family or as `drop_original: false` attached to an unrelated summary. + +## Complete example + +For the PromQL query: ```promql -quantile_over_time(0.95, request_duration_seconds[5m]) +quantile_over_time(0.95, request_duration_seconds{region="us-east"}[5m]) ``` -The precise PromQL-to-summary choice is supplied by ASAPPlanner and the -ASAPQuery-backend control plane; the collector only applies the delivered -execution configuration. - -### Proposed: align these fields with the post-ASAP DAG's own vocabulary - -**Not implemented.** The table above matches -`opentelemetry-collector-contrib-patch/processor/asapedgeprocessor/config.go` -today. That struct predates ASAPPlanner's current `post_asap` IR and -reinvents two vocabularies instead of naming it directly: `mode` is a bare -string with exactly two values (`per_series`/`whole_stream`) and -`aggregate_by` is a plain string list — neither can distinguish -`Reduction::PerEntity` from a genuine zero-key `Reduce`, and `family` has no -discriminator for the four non-`Sum` exact kinds or any field for -`GroupingStrategy` (Hydra). ASAPQuery-backend's compiled-plan design -proposes renaming/extending `metrics[]` so every field and enum value is -spelled directly from `post_asap` — -`source`/`family`/`exact_kind`/`reduce_by`/`reduce_without`/`per_entity`/ -`grouping`/`hydra_kind`/`shared_rows`/`shared_columns` — as a flat, -`mapstructure`-shaped struct (matching this same file's existing -`FamilyKind`/`Tier`/`ColdFormat` pattern), not a nested tagged union. See -[§5 of that design](https://github.com/ProjectASAP/ASAPQuery-backend/blob/docs/asapplanner-workload-planner-migration/control_plane/docs/design-compiled-plan-collector-backend-split.md#5-redesigning-asap_edgemetrics-to-match-the-dags-own-shape) -for the full field-by-field redesign and a worked example. This section -should be rewritten against the table above once that redesign lands in -`config.go`, with old field names accepted as deprecated aliases for one -release. - -## Apply behavior and response - -On receipt, ASAPCollector: - -1. selects the empty-key entry, then `"asap-otel"`, then another non-empty - entry as a compatibility fallback; -2. parses its body as YAML; -3. writes the resulting complete YAML to the configured remote-config path; -4. reports the remote-config status; and -5. exits when a changed configuration requires the supervisor to restart it. - -If the received YAML is identical to the configuration already on disk, the -collector reports it as applied without requesting another restart. - -The collector response contains: +ASAPPlanner may produce a candidate containing a DDSketch `SummaryAgg` over +the sample value, `Reduction::PerEntity`, and a quantile +`SummaryEstimate { q: 0.95 }`. After selecting that candidate and placing the +aggregation at the collector, ASAPQuery-backend may send: -| Field | Definition | -| --- | --- | -| `last_remote_config_hash` | The `config_hash` of the configuration being reported. | -| `status` | `APPLIED` when the YAML was accepted and written (or was already identical), otherwise `FAILED`. | -| `error_message` | Parsing or file-application error when status is `FAILED`. | +```yaml +api_version: asap.io/v1alpha1 +kind: CollectorPlan +metadata: + plan_id: workload-dashboard-a + revision: 42 + content_hash: sha256:6d9f... + generated_at: 2026-08-27T20:00:00Z + valid_from: 2026-08-27T20:00:05Z + expires_at: 2026-08-28T20:00:05Z + planner_revision: 7278505 + candidate_id: candidate-quantile-ddsketch + query_ids: [dashboard-latency-p95] +target: + instance_uid: 550e8400-e29b-41d4-a716-446655440000 + capability_hash: sha256:a31c... +on_unsupported: reject_plan +materializations: + - id: mat:sha256:98f1... + logical_node_ref: summary-agg-7 + input: + metric: request_duration_seconds + matchers: + - {label: region, op: eq, value: us-east} + value: sample_value + summary: + family: sketch + algorithm: ddsketch + parameters: {alpha: 0.01} + accuracy: {kind: epsilon, epsilon: 0.01} + reduction: + kind: per_entity + window: + kind: tumbling + size: 1m + slide: 1m + allowed_lateness: 10s + placement: + stage: collector + shards: 4 + transmission: + mode: delta + encoding: asap.summary.ddsketch + schema_version: 1 + emit_every: 10s + full_checkpoint_every: 1m + sequence_scope: materialization_window_producer + output: + endpoint_ref: asapquery-primary +``` -`APPLIED` does not by itself prove that the restarted collector loaded the -configuration, processed observations, or emitted the requested summaries. -The MVP harness must additionally capture collector startup, active processor -configuration, processing counters, emitted payloads, and backend query -evidence. +The matching backend plan uses the same `plan_id`, `revision`, and +materialization ID. It records DDSketch with `alpha: 0.01`, the source/filter, +per-entity reduction, compatible windows, storage route, and the quantile +readout. Query time reads that decision; it must not run ASAPPlanner again and +independently choose KLL or different DDSketch parameters. -## JSON HTTP polling is a different interface +## Validation and atomic application -The repository also has an HTTP polling control channel. It returns a JSON -`PrecomputeConfigSet`, whose current top-level Go field names are: +Before activation, ASAPCollector validates: -| JSON field | Definition | -| --- | --- | -| `Version` | Monotonically increasing HTTP plan version. | -| `Configs` | List of runtime precompute configurations. | +1. schema version, target instance, revision ordering, content hash, lifetime, + and capability hash; +2. unique materialization IDs and query/node traceability; +3. source matcher and value-input types; +4. summary family/algorithm/parameter/accuracy compatibility; +5. reduction and window invariants; +6. transmission support, including delta checkpoint and sequence rules; and +7. exporter references and resource guardrails. -Its acknowledgement body is: +The plan is all-or-nothing. An invalid materialization rejects the candidate; +the collector keeps the previous unexpired plan. A valid plan is staged and +activated atomically at `valid_from`. Existing windows follow an explicitly +reported transition policy; state from incompatible revisions is never merged. -```json -{"plan_version": 42} -``` +Re-delivery of the same `(plan_id, revision, content_hash)` is idempotent. An +older revision is rejected. Reusing `(plan_id, revision)` with another hash is +rejected. An expired plan stops producing state unless a separately configured, +bounded last-known-good policy explicitly permits a grace interval. -This JSON contract is not embedded in the OpAMP YAML body. The runtime -`OpAmpChannel` implementation is currently a stub, so documentation and tests -must not claim that `PrecomputeConfigSet` is delivered through OpAMP today. - -## Current contract gap - -The design requires one compatible plan for the collector and ASAPQuery data -plane, but the implemented OpAMP YAML schema currently has no explicit -`plan_id`, `plan_version`, activation time, expiry time, or backend -compatibility identifier. `config_hash` identifies the remote collector -configuration; it is not a complete versioned end-to-end plan contract. - -Until those fields are implemented and validated, the MVP harness must record -the OpAMP `config_hash` and independently prove which backend plan was active. -It must not infer plan ordering, expiry, or collector/backend compatibility -from `APPLIED` alone. - -ASAPQuery-backend's compiled-plan design (linked above) proposes closing this -gap by having the control plane's compile step stamp `plan_id`/ -`plan_version`/`activation`/`expiry`/`backend_compat` identically onto both -the collector configuration this document describes and the companion -`BackendPlan` it compiles alongside it, so the two can be checked for -agreement directly instead of only through `config_hash`. That design does -not yet specify where in this document's OpAMP envelope those fields should -live — a sibling key next to `processors.asap_edge`, a field inside -`asap_edge` itself, or a separate `AgentConfigFile` entry are all still -open — and none of them are implemented on this wire today. This section -should be updated to document the chosen encoding once that decision is -made and implemented here, not before. - -## Failure behavior - -| Scenario | Current required result | +## Application report + +OpAMP `RemoteConfigStatus` reports delivery/application of the config hash, but +the ASAP contract needs a semantic application report as well. The collector +returns a typed OpAMP custom message with: + +| Field | Definition | +| --- | --- | +| `plan_id`, `revision`, `content_hash` | Candidate being reported. | +| `remote_config_hash` | OpAMP configuration hash that carried it. | +| `status` | `rejected`, `staged`, `active`, `expired`, or `failed`. | +| `observed_at`, `activated_at` | Status and activation timestamps. | +| `active_materialization_ids` | Exact materializations installed. | +| `effective_capability_hash` | Collector capabilities used during validation. | +| `errors[]` | Machine-readable `code`, field `path`, and human-readable message. | + +The custom-message capability is `io.asap.collector.plan.v1`; message type is +`application_report`; its data is protobuf-encoded. `RemoteConfigStatus.APPLIED` +without an `active` application report does not prove semantic activation. +The MVP harness must additionally observe post-activation input, emitted +payloads carrying the same identities, and successful backend ingestion. + +## Fail-closed behavior + +| Condition | Required result | | --- | --- | -| Missing configuration map or non-empty body | Log and ignore the message. Because the current code sends no failure status, the MVP harness must fail on its missing application evidence. | -| Body is not valid YAML | Report `FAILED`; do not write it as the active file. | -| Remote-config file cannot be written | Report `FAILED` with the error. | -| Changed YAML is written | Report `APPLIED`; supervisor restart is still required. | -| Collector fails after restart | Treat the MVP scenario as failed even if OpAMP previously reported `APPLIED`. | -| Backend applies an incompatible plan | Reject the emitted state or fail the run; never return a plausible mismatched result. | +| Unknown schema version, enum, or required field | Reject the entire plan. | +| Candidate uses a Planner summary unsupported by the collector | Control plane must choose another candidate or exact fallback; collector rejects if still sent. | +| Family, algorithm, and parameters disagree | Reject; never substitute defaults. | +| `per_entity`/`reduce` semantics are ambiguous | Reject. | +| Delta requested for an incompatible family/encoding | Reject. | +| Backend plan lacks the same materialization identity and contract | Do not activate the bundle or reject emitted state. | +| Revision is stale, conflicting, premature, or expired | Preserve the current valid plan and report the reason. | +| Application evidence is missing | MVP verdict is FAIL, not UNKNOWN or PASS. | + +## Current implementation gap + +The current ASAPCollector OpAMP extension does not yet implement this target +interface. Today it: + +- reads an arbitrary `AgentConfigFile.body` as a complete OTel Collector YAML; +- writes that YAML to a configured path and relies on a supervisor restart; +- identifies it only by OpAMP `config_hash`; and +- reports `APPLIED` after a syntactic YAML check and file write. + +It does not yet parse a versioned `CollectorPlan`, validate Planner-derived +semantics, apply a plan atomically in-process, or report plan/materialization +identities. The separate JSON `PrecomputeConfigSet` HTTP polling interface is +also not this target contract; the runtime OpAMP control-channel adapter is +currently a stub. + +Until the target interface is implemented, tests must not claim that +`config_hash` or `APPLIED` proves a compatible versioned plan was active. ## Non-goals -This document does not define PromQL parsing, query-to-summary algebra, -ASAPPlanner optimization rules, summary payload encoding, or -ASAPQuery-backend query execution. It defines only the control-plane-to- -collector configuration boundary and the evidence needed to validate it. +This interface does not serialize ASAPPlanner's internal DAG, define PromQL +parsing, rank Planner candidates, define backend query execution, or specify +summary-state byte encoding. It defines the physical control-plane boundary +between ASAPQuery-backend and ASAPCollector and the identities that bind it to +the corresponding backend plan. From 834d10665e3425ed9dae03e70662c4410e199de1 Mon Sep 17 00:00:00 2001 From: zz_y Date: Thu, 27 Aug 2026 15:39:55 -0600 Subject: [PATCH 6/6] docs: align collector plan with compiled plan contract --- docs/developer_docs/opamp-config-push.md | 88 ++++++++++++++++-------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/docs/developer_docs/opamp-config-push.md b/docs/developer_docs/opamp-config-push.md index ce6fe8e2..e8826a05 100644 --- a/docs/developer_docs/opamp-config-push.md +++ b/docs/developer_docs/opamp-config-push.md @@ -34,6 +34,10 @@ the whole plan atomically and reports the active plan identity. This document defines the target interface. The final section distinguishes it from the narrower interface implemented today. +It is the collector-side companion to ASAPQuery-backend's +[compiled-plan split design](https://github.com/ProjectASAP/ASAPQuery-backend/blob/docs/asapplanner-workload-planner-migration/control_plane/docs/design-compiled-plan-collector-backend-split.md). +Both documents use the same plan identity and physical-planning boundary. + ## Ownership boundary | Component | Owns | Must not own | @@ -62,6 +66,8 @@ selected candidate's semantics: | `col` | `input.value` or `input.item_label` | Readout input metadata | | `Reduction::PerEntity` | `reduction.kind: per_entity` | Preserve each source series identity | | `Reduction::Reduce(GroupKeys)` | `reduction.kind: reduce` plus explicit `by` or `without` labels | Matching group/roll-up shape | +| `GroupingStrategy::PerSubpopulationInstance` | `grouping.kind: per_subpopulation_instance` | One independent state instance per reduction group | +| `GroupingStrategy::SharedMultiSubpopulation` | `grouping.kind: shared_multi_subpopulation` plus Hydra kind and parameters | The identical shared-state layout and readout contract | | Time-range semantics around `SummaryAgg` | Concrete streaming `window` and lateness policy | Compatible query-window/read range | | `SummaryEstimate` | No collector readout; collector emits summary state | Query-time readout, such as quantile, cardinality, point count, or top-k | | `Logical` subtree | Raw pass-through only when the physical plan explicitly selects it | Exact execution or configured fallback | @@ -84,8 +90,8 @@ The interface has two layers: The OpAMP `AgentConfigMap` entry is named `asap-collector-plan.yaml`. A receiver must select this exact entry; it must not silently choose an arbitrary first file. `AgentRemoteConfig.config_hash` is the hash used by OpAMP delivery. It is -separate from `metadata.content_hash`, which identifies the canonical plan -content across transports and processes. +separate from `metadata.plan_id`, which identifies the compiled plan across +transports and processes. The plan is YAML because it is an operator-visible configuration artifact and fits OpAMP's configuration-file model. YAML is only the serialization: fields @@ -102,23 +108,25 @@ receiver must never guess it. | --- | --- | --- | --- | | `api_version` | string | yes | Schema version. MVP value: `asap.io/v1alpha1`. | | `kind` | string | yes | Must be `CollectorPlan`. | -| `metadata.plan_id` | string | yes | Identity shared by this collector plan and its matching backend plan. | -| `metadata.revision` | uint64 | yes | Monotonically increasing revision for this target. | -| `metadata.content_hash` | string | yes | SHA-256 of the canonical plan with this field omitted. | -| `metadata.generated_at` | RFC 3339 timestamp | yes | Time the control plane produced this revision. | -| `metadata.valid_from` | RFC 3339 timestamp | yes | Earliest activation time. | -| `metadata.expires_at` | RFC 3339 timestamp | yes | Time after which the collector must stop using the plan. | +| `metadata.plan_id` | string | yes | Content-addressed identity of the selected DAG, topology identity, and semantic constraints, shared with the matching backend plan. Mutable sizing/lifecycle settings are excluded. | +| `metadata.plan_version` | uint64 | yes | Monotonic version within a `plan_id`, bumped when the same selection is recompiled, for example after resizing. | +| `metadata.generated_at` | RFC 3339 timestamp | yes | Time the control plane produced this plan version. | +| `metadata.activation` | RFC 3339 timestamp | yes | Earliest time at which this plan becomes authoritative. | +| `metadata.expiry` | RFC 3339 timestamp or null | yes | Optional time after which the collector must stop using the plan; null means until superseded. | +| `metadata.backend_compat` | string | yes | Required backend plan and emitted-state schema compatibility identity. | | `metadata.planner_revision` | string | yes | ASAPPlanner commit/version used to create the candidate DAG. | | `metadata.candidate_id` | string | yes | Stable identifier of the selected candidate post-ASAP plan. | | `metadata.query_ids` | list of strings | yes | Workload queries whose selected plan requires these materializations. | | `target.instance_uid` | string | yes | Exact OpAMP agent instance this plan targets. | +| `target.edge_id` | string | yes | Deployment edge assignment produced by the physical allocator. | | `target.capability_hash` | string | yes | Capability snapshot against which the physical plan was validated. | | `on_unsupported` | enum | yes | `reject_plan` for MVP. No silent downgrade or substitution is allowed. | | `materializations` | list | yes | Collector-side producers. An empty list is valid only for an explicit no-op plan. | -`plan_id` groups compatible collector and backend portions. `revision` orders -updates. `content_hash` makes a revision immutable. Reusing the same -`(plan_id, revision)` with different content is an error. +`plan_id` identifies the compiled structure. `plan_version` orders recompiles +of that structure. Reusing the same `(plan_id, plan_version)` for different +collector bytes is an error; the OpAMP `config_hash` identifies those exact +bytes. ### Materialization identity and input @@ -141,7 +149,7 @@ rejected during physical planning. | --- | --- | --- | --- | | `summary.family` | enum | yes | `exact_aggregate`, `sketch`, `sample`, `wavelet`, or `stat_model`, matching Planner `SummaryFamilyType`. | | `summary.algorithm` | enum | yes | Concrete algorithm within the family. | -| `summary.parameters` | tagged object | yes | Parameters belonging to exactly that algorithm. Empty object for parameterless exact accumulators. | +| `summary.parameters` | object | yes | Typed parameters belonging to exactly that algorithm. Empty object for parameterless exact accumulators. | | `summary.accuracy` | tagged object | yes | Original Planner constraint: `exact`, `epsilon`, or `epsilon_delta`. | When a Planner exact aggregation carries no explicit accuracy field, the @@ -183,13 +191,16 @@ summary: The control plane must validate the algorithm/parameter pair against the selected Planner `SketchKind`. A DDSketch with KLL's `k` parameter is invalid. -### Reduction and windows +### Reduction, grouping, and windows | Field | Type | Required | Definition | | --- | --- | --- | --- | | `reduction.kind` | enum | yes | `per_entity` or `reduce`; preserves Planner's distinction. | | `reduction.by` | list of strings | for `reduce` | Labels retained when `without` is false. An empty list means a real global reduction. | | `reduction.without` | boolean | for `reduce` | When true, `by` names excluded labels and all other labels are retained. | +| `grouping.kind` | enum | yes | `per_subpopulation_instance` or `shared_multi_subpopulation`, matching Planner `GroupingStrategy`. | +| `grouping.hydra_kind` | enum | for shared grouping | `hydra_cms` or `hydra_count_sketch` for Planner alternatives with a modeled error guarantee. | +| `grouping.parameters` | object | for shared grouping | Hydra parameters, including the inner sketch dimensions and shared structure dimensions. | | `window.kind` | enum | yes | `tumbling` for the MVP. | | `window.size` | duration | yes | Logical summary window. | | `window.slide` | duration | yes | Window start interval; equal to `size` for tumbling windows. | @@ -200,6 +211,16 @@ keeps one result per input series; the latter merges every matching series into one global group. This distinction comes directly from ASAPPlanner's `Reduction` type and must survive physical lowering. +Reduction and grouping are orthogonal. Reduction defines which logical +subpopulations exist; grouping defines whether each subpopulation owns an +independent summary instance or shares a multi-subpopulation structure. For +example, a CMS reduced by `region` can use either one CMS per region or one +Hydra-CMS serving all regions. Shared grouping is invalid for +`Reduction::PerEntity` and for algorithms without a Planner-approved shared +variant. The MVP collector may advertise only `per_subpopulation_instance`; +if so, the control plane must select another candidate rather than erase a +Planner-selected shared grouping strategy. + Planner time-range semantics describe what the query means. The physical control plane selects a concrete streaming window representation capable of answering that range. If the chosen windows cannot compose to the Planner @@ -223,7 +244,7 @@ be deployed. Transmission is a physical decision made by ASAPQuery-backend, not by ASAPPlanner. `full` and `delta` change representation, not logical query semantics. Delta is legal only when the advertised algorithm/state encoding -supports it. Each delta payload must carry plan ID, revision, materialization +supports it. Each delta payload must carry plan ID, plan version, materialization ID, window identity, producer identity, sequence number, and base/checkpoint identity so the backend can reject gaps or incompatible state. @@ -248,17 +269,18 @@ aggregation at the collector, ASAPQuery-backend may send: api_version: asap.io/v1alpha1 kind: CollectorPlan metadata: - plan_id: workload-dashboard-a - revision: 42 - content_hash: sha256:6d9f... + plan_id: sha256:6d9f... + plan_version: 42 generated_at: 2026-08-27T20:00:00Z - valid_from: 2026-08-27T20:00:05Z - expires_at: 2026-08-28T20:00:05Z + activation: 2026-08-27T20:00:05Z + expiry: 2026-08-28T20:00:05Z + backend_compat: asap.backend-plan.v1 planner_revision: 7278505 candidate_id: candidate-quantile-ddsketch query_ids: [dashboard-latency-p95] target: instance_uid: 550e8400-e29b-41d4-a716-446655440000 + edge_id: edge-a capability_hash: sha256:a31c... on_unsupported: reject_plan materializations: @@ -276,6 +298,8 @@ materializations: accuracy: {kind: epsilon, epsilon: 0.01} reduction: kind: per_entity + grouping: + kind: per_subpopulation_instance window: kind: tumbling size: 1m @@ -295,7 +319,8 @@ materializations: endpoint_ref: asapquery-primary ``` -The matching backend plan uses the same `plan_id`, `revision`, and +The matching backend plan uses the same `plan_id`, `plan_version`, +`backend_compat`, and materialization ID. It records DDSketch with `alpha: 0.01`, the source/filter, per-entity reduction, compatible windows, storage route, and the quantile readout. Query time reads that decision; it must not run ASAPPlanner again and @@ -305,24 +330,27 @@ independently choose KLL or different DDSketch parameters. Before activation, ASAPCollector validates: -1. schema version, target instance, revision ordering, content hash, lifetime, +1. schema version, target instance, plan-version ordering, OpAMP config hash, + lifetime, backend compatibility, and capability hash; 2. unique materialization IDs and query/node traceability; 3. source matcher and value-input types; 4. summary family/algorithm/parameter/accuracy compatibility; -5. reduction and window invariants; +5. reduction, grouping, and window invariants; 6. transmission support, including delta checkpoint and sequence rules; and 7. exporter references and resource guardrails. The plan is all-or-nothing. An invalid materialization rejects the candidate; the collector keeps the previous unexpired plan. A valid plan is staged and -activated atomically at `valid_from`. Existing windows follow an explicitly -reported transition policy; state from incompatible revisions is never merged. +activated atomically at `activation`. Existing windows follow an explicitly +reported transition policy; state from incompatible plan versions is never +merged. -Re-delivery of the same `(plan_id, revision, content_hash)` is idempotent. An -older revision is rejected. Reusing `(plan_id, revision)` with another hash is -rejected. An expired plan stops producing state unless a separately configured, -bounded last-known-good policy explicitly permits a grace interval. +Re-delivery of the same `(plan_id, plan_version, config_hash)` is idempotent. An +older plan version is rejected. Reusing `(plan_id, plan_version)` with another +config hash is rejected. An expired plan stops producing state unless a +separately configured, bounded last-known-good policy explicitly permits a +grace interval. ## Application report @@ -332,7 +360,7 @@ returns a typed OpAMP custom message with: | Field | Definition | | --- | --- | -| `plan_id`, `revision`, `content_hash` | Candidate being reported. | +| `plan_id`, `plan_version`, `backend_compat` | Candidate being reported. | | `remote_config_hash` | OpAMP configuration hash that carried it. | | `status` | `rejected`, `staged`, `active`, `expired`, or `failed`. | | `observed_at`, `activated_at` | Status and activation timestamps. | @@ -356,7 +384,7 @@ payloads carrying the same identities, and successful backend ingestion. | `per_entity`/`reduce` semantics are ambiguous | Reject. | | Delta requested for an incompatible family/encoding | Reject. | | Backend plan lacks the same materialization identity and contract | Do not activate the bundle or reject emitted state. | -| Revision is stale, conflicting, premature, or expired | Preserve the current valid plan and report the reason. | +| Plan version is stale or conflicting, or activation/expiry disallows use | Preserve the current valid plan and report the reason. | | Application evidence is missing | MVP verdict is FAIL, not UNKNOWN or PASS. | ## Current implementation gap