Skip to content

fix(emit): deterministic YAML via BTreeMap (kills agent apply-loop) - #281

Merged
zzylol merged 1 commit into
mainfrom
controller-deterministic-yaml-emit
May 18, 2026
Merged

zzylol merged 1 commit into
mainfrom
controller-deterministic-yaml-emit

Conversation

@zzylol

@zzylol zzylol commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Controller's YAML emit was non-deterministic (HashMap iteration order). Same semantic content emitted as byte-different YAML on each call. The agent's opampextension byte-level no-op check (ASAPCollector#381) saw "different bytes" on every reconnect, applied + restarted, the controller resent the same semantic config, and the agent looped forever. 10+ restarts per smoke test before any sketch could flush.

Fix: 6 HashMap fields on CollectorYaml / ServiceSectionBTreeMap. Iterate in key order. Same semantic content always serializes to the same bytes.

Matches the controller's content-addressed identity story (PolicyFingerprint::from_config already sorts hash inputs; SeriesIdResolver keys are sorted attribute fingerprints). The emit was the last non-deterministic hop.

Test plan

  • cargo test -p control_plane --lib → 706/706 pass
  • Empirically verified: curl /api/v1/collector-config/agent twice — pre-fix, key ordering differs; post-fix, identical bytes
  • End-to-end smoke verification: agent should now restart ONCE (bootstrap → controller-emit) and stay up, not loop

🤖 Generated with Claude Code

The controller's collector-yaml emit used `HashMap<String, Value>`
for `processors`, `receivers`, `extensions`, `connectors`,
`exporters`, and `pipelines`. serde_yaml iterates a HashMap in
Rust's randomized order, so calling `emit_edge_yaml` /
`emit_edge_yaml_5sketch_routing` / `emit_gateway_yaml` twice
on the same input produced byte-different YAML strings — same
semantic content, different key ordering.

This broke the agent's opampextension byte-level no-op check
(ASAPCollector#381 + the Issue #4 follow-up): the agent saw
"incoming bytes != on-disk bytes" on every reconnect, applied,
restarted, the controller pushed the SAME semantic config again,
and the agent looped. 10+ restarts per smoke test before any
sketch could flush.

Fix: switch the six HashMap fields on `CollectorYaml` /
`ServiceSection` to `BTreeMap`. BTreeMap iterates in key order;
serde_yaml's emit is now deterministic. Same semantic content
always serializes to the same bytes.

Bonus: this matches the controller's broader content-addressed
identity story — `PolicyFingerprint::from_config` already sorts
its hash inputs into canonical order, and `SeriesIdResolver`
keys are sorted attribute fingerprints. The emit was the last
non-deterministic hop.

The agent's no-op check (ASAPCollector PR for Issue #4) is
still useful as defense-in-depth — handles non-controller
OpAMP servers that might still push slightly different bytes
for the same intent.

Test plan:
  * `cargo test -p control_plane --lib`: 706/706 pass.
  * Empirically verified pre-fix: `curl
    /api/v1/collector-config/agent` twice → key ordering differs
    (processors block: countmin / countsketch / transform/keep_for_*
    appear in different positions). Post-fix: identical bytes
    across calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 603ee35 into main May 18, 2026
@zzylol
zzylol deleted the controller-deterministic-yaml-emit branch July 17, 2026 20:05
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