fix(moq-pub-mmtp): reconnect with backoff on relay session loss (BLO-26173) - #76
Conversation
…26173)
production-blockcastd/blockcastd-moq-pub-mmtp — the live Solana shred
publisher — was crash-restarting several times a day: any relay session
error (observed as a WebTransport timeout) propagated out of main() and
exited(1), relying on restartPolicy: Always to recover. Each restart is a
full publish outage and forces the SSM (S,G) join to be re-issued from
scratch.
main() now holds the UDP socket (or stdin) open for the life of the
process and retries the moq-transport connect/session/publish_namespace
lifecycle in a loop with exponential backoff (500ms..30s, both
CLI-tunable) instead of exiting. Because the input source is opened once
outside the retry loop and only ever borrowed per attempt, a UDP source's
SSM join survives a relay reconnect without a pod restart. Malformed
packet / framing errors remain fatal, unchanged from before — only
relay-session-lifecycle endings (connect failure, session end, namespace
publish end) trigger a retry.
Reconnects are observable via a `moq_pub_session_reconnect_total{task}`
counter (metrics facade, mirrors moq-native-ietf's moq_negotiation_total)
plus a warn/info log line per attempt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ometheus cfg gate CI on this PR was failing (build + heap-profile-image jobs) with a dead-code -D warnings error: describe_metrics() was only called from inside the #[cfg(feature = "metrics-prometheus")] install-success arm, so any build without that feature (the default `cargo test`, and the heap-profiling feature build, neither of which enable metrics-prometheus) never referenced it. describe_counter! is a no-op against the facade when no recorder is installed, so moving the call above the feature-gated block is safe and keeps the function reachable under every feature combination. Also fixes a latent gap: descriptions are now registered whenever MOQ_PUB_METRICS_ADDR is set, not only after the Prometheus exporter installs successfully. Pre-existing on main from the BLO-22882 merge; fixed here since it was blocking this PR's CI.
|
CI ( Pushed No inline review findings were posted against the reconnect/backoff logic itself ( |
Independent review (PlatformSREEngineer, non-author pass)I read the full diff against Verification performed:
One non-blocking finding, for a follow-up rather than this PR:
I'm not fixing it in this PR — the natural fix (reset Disposition: I wrote this code, so I'm not the independent reviewer this PR actually needs (per BLO-26173's own note: "needs a real review pass, not a rubber stamp") — this comment is my self-review pass to catch what I could before handing it to a human. Automated Copilot review requests aren't landing on this repo (checked |
Summary
BLO-26173:
production-blockcastd/blockcastd-moq-pub-mmtp— the live public Solana shred publisher — has no reconnect/retry on relay session loss. It propagates the error out ofmain, exits 1, and relies onrestartPolicy: Always. Observed cause in production:webtransport error: session error: connection error: timed out. The relay side was healthy (0 restarts, 6d+ uptime) — the session loss is one-sided. Each restart is a full publish outage, and forces the SSM(S,G)join for232.0.0.1:5001to be re-issued from scratch.This also voids the "recovering it would require restarting a live production publisher" premise recorded on BLO-22347/BLO-22882 — the pod already restarts itself several times a day.
What changed
main.rs: the UDP socket (or stdin) is now opened once and held for the life of the process; every relay reconnect below only ever borrows it. The moq-transport connect → session →publish_namespacelifecycle runs in a loop with exponential backoff (500ms → 30s, both CLI-tunable via--reconnect-backoff-{min,max}-ms) instead of exiting on error.(S,G)join survives a relay reconnect without a pod restart (acceptance criterion: "re-established (or held) across a reconnect").main— this PR only changes how relay-session-lifecycle endings (connect failure, session end,publish_namespaceend) are handled.TracksReaderis single-use perPublisher::publish_namespacecall) — pure, catalog-only, no I/O, so a failure there is still treated as fatal (schema/config bug), not retried.moq_pub_session_reconnect_total{task}counter (via themetricsfacade, same pattern as moq-native-ietf'smoq_negotiation_total) plus awarn/infolog line per attempt with the task name, attempt count, and backoff — so a reconnect storm is distinguishable from a healthy long session.cli.rs: adds--reconnect-backoff-min-ms(default 500) /--reconnect-backoff-max-ms(default 30000).Test plan
cargo test -p moq-pub-mmtp— 79 tests pass (added:backoff_duration_doubles_and_caps,describe_task_end_distinguishes_clean_end_from_error_and_panic,stdin_input_reports_exhausted_on_clean_eof,stdin_input_yields_packet_for_one_frame, updatedudp_recv_dispatches_one_packetfor the newnext_input_eventsplit, plus a CLI defaults test for the new backoff flags)cargo fmt -p moq-pub-mmtp -- --checkcleancargo clippy -p moq-pub-mmtp --bin moq-pub-mmtp --tests— zero new warnings (the pre-existing 7 warnings inmmtp_parse.rs/publish.rs/the example binary are unrelated to this diff and unchanged)kubectl -n production-blockcastd get pod -l app.kubernetes.io/name=moq-pub-mmtprestart count flat across an observed session-timeout event, pluslogs deploy/blockcastd-moq-pub-mmtp | grep -iE 'reconnect|session error'showing an in-process reconnect with no matching container restart.Blast radius
This changes the publisher's session lifecycle on the live publish datapath (per the issue: "needs a real review pass, not a rubber stamp"). Scope was kept deliberately narrow: only the relay-session retry/backoff behavior is new; packet parsing, dispatch, and catalog/track construction logic are untouched.
🤖 Generated with Claude Code