Skip to content

feat(broker): broadcast config changes to all replicas via fanout - #628

Draft
mahlunar wants to merge 1 commit into
masterfrom
dmat-365-multi-replica-fanout-broadcast
Draft

feat(broker): broadcast config changes to all replicas via fanout#628
mahlunar wants to merge 1 commit into
masterfrom
dmat-365-multi-replica-fanout-broadcast

Conversation

@mahlunar

@mahlunar mahlunar commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Enable running the release-manager as N replicas with fresh reads on every replica, implementing the fanout-broadcast approach from docs/multi-replica-analysis.md.

Today the read APIs (/status, /describe/*, /policies) serve from a per-replica local clone that is refreshed only when that replica receives the GitHub push webhook. Behind a load balancer the webhook hits exactly one replica, so the others serve stale data indefinitely. This change makes every replica sync on every push via a fanout broadcast, with a coarse ticker as a backstop.

Changes

  • Broadcast primitive on broker.BrokerPublishBroadcast and StartBroadcastConsumer. The amqpextra impl backs them with a dedicated fanout exchange and a per-replica server-named, auto-delete queue (every replica receives every message; queue is removed on disconnect). Deliveries are acked unconditionally — skip-if-current makes the handler idempotent. The memory broker fans out in-process for dev/test.
  • Low-level AMQP knobsConsumerConfig.Fanout (fanout exchange + server-named/auto-delete queue, no single-active-consumer) and PublishDto.ExchangeType (defaults to topic). The existing flow-queue declaration (quorum + SAC) is byte-for-byte unchanged.
  • events.ConfigChangedEvent{SHA} implementing broker.Publishable, and git.Service.MasterHash(ctx) reading HEAD under the existing RLock.
  • Wiring — the webhook does SyncMaster → read MasterHash → broadcast the SHA (log-only on failure, still returns 200), via an injected hook matching the existing publish-hook idiom. start.go starts the broadcast consumer (skip-if-current, else SyncMaster) and a ~60s ticker backstop in their own goroutines.
  • New flags--amqp-broadcast-exchange (default release-manager-broadcast) and --config-repo-sync-interval (default 60s).
  • Tests: round-trip test for ConfigChangedEvent and a behavioral publish→consume test for the memory broadcast path.

Why

Write flows are already multi-replica safe (the AMQP queue is a quorum queue with x-single-active-consumer, giving HA failover). Stale reads on replicas that didn't receive the webhook are the only blocker to running N replicas — this closes that gap with sub-2s freshness propagation while leaving the write path untouched.

Notes for reviewers

  • Queue declaration deviation: the design specified a queue-level exclusive: true. The makasim/amqpextra consumer treats queue-source options as mutually exclusive, so WithExchange is used instead — yielding a server-named, auto-delete queue bound to the fanout exchange. Functionally equivalent here (per-replica, auto-cleaned, no SAC, receives every message); only connection-level exclusivity is dropped, which adds nothing given the queue name is server-generated and only this consumer binds to it.
  • Self-reviewed across concurrency, AMQP/messaging, and general-correctness lenses. The Prefetch on the broadcast consumer was set to 1 (backpressure for the single worker) and a fanout-mode shutdown log line was corrected as part of that review. No blocking issues found.

Closes DMAT-365


Note

Medium Risk
Touches AMQP topology and fleet-wide git sync behavior; misconfiguration or broadcast failures could leave replicas stale until the ticker heals, but write flows and the existing quorum SAC queue are unchanged.

Overview
Adds multi-replica config freshness so every pod’s local config clone stays aligned after a config-repo push, not only the replica that receives the GitHub webhook.

Broker broadcast path: broker.Broker gains PublishBroadcast / StartBroadcastConsumer. AMQP uses a separate fanout exchange and per-replica server-named auto-delete queues (ConsumerConfig.Fanout, configurable ExchangeType on publish). The in-memory broker mirrors this for dev/tests.

Event + git: New ConfigChangedEvent carries the new master SHA; replicas call MasterHash and skip SyncMaster when already current.

Wiring: After webhook SyncMaster, the handler reads HEAD and broadcasts via an injected hook (failures are log-only, still HTTP 200). start.go runs a broadcast consumer plus a ~60s SyncMaster ticker backstop. Flags: --amqp-broadcast-exchange, --config-repo-sync-interval.

Docs/tests: docs/multi-replica-analysis.md documents the design; tests cover the event and memory broadcast delivery.

Reviewed by Cursor Bugbot for commit a9a62d2. Configure here.

Enable running release-manager as N replicas with fresh reads on every
replica. On a config-repo push the GitHub webhook syncs master, reads the
new HEAD, and broadcasts a ConfigChangedEvent over a dedicated fanout
exchange. Each replica binds its own server-named, auto-delete queue and
syncs its local clone unless its HEAD already matches (skip-if-current).
A ~60s ticker backstops missed broadcasts.

The existing quorum + single-active-consumer flow queue is unchanged, so
write flows stay serialized fleet-wide with HA failover.

Closes DMAT-365

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant