feat(new-protocol): tear down legacy stack after cutover decides#4714
Conversation
After LEGACY_SHUTDOWN_DECIDE_COUNT new-protocol decides, abort the legacy-event forwarders and shut down the legacy consensus tasks and network, leaving the shared membership coordinator (in-flight DRB computations) and in-memory legacy state intact. Outbound external messages switch to the coordinator's network once it is running. They are framed in the same versioned Message envelope as the legacy path, since the coordinator sends external payloads over the wire verbatim and the receiver's fallback decoder expects that framing; sending bare payload bytes made peers decode garbage lengths and abort on exabyte-scale allocations. On legacy network shutdown, release the combined network's dedup cache and per-view channels, which stay pinned after the cutover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Claude finished @bfish713's task in 3m 53s —— View job Claude Code ReviewReviewed the full diff against Findings1. The comment claims "Nothing sends these today", but broadcast external messages are produced today:
Post-cutover these hit the 2. The automatic decide-count trigger is not covered by the test (low / test-coverage) — inline
Review-focus answersExternal-message framing in Forwarder-teardown vs. a peer crossing the cutover — I believe this is safe. The three forwarders ( Minor notes (non-blocking)
Nice
|
| // Nothing sends these today: all catchup requests are batched | ||
| // direct messages, and the coordinator's network has no broadcast | ||
| // topic for external messages. | ||
| other => { | ||
| tracing::warn!( | ||
| message = ?other, | ||
| "dropping unsupported external message after cutover" | ||
| ); | ||
| }, |
There was a problem hiding this comment.
The comment "Nothing sends these today" is not accurate — broadcast external messages are sent today.
request_vid_shares issues a RequestType::Broadcast request (api.rs:611-613), which the request-response protocol routes through Sender::send_broadcast_message (request_response/network.rs:40) → OutboundMessage::Broadcast(MessageKind::External(..)) (request-response/src/lib.rs:401-412).
Post-cutover, those broadcasts land in this other => arm and are silently dropped (warn + eventual 40s timeout in the caller). So an availability VID-share query that needs to fetch shares from peers will fail once the legacy stack is torn down. This is precisely the follow-up "confirm nothing relies on request-response catchup post-cutover."
Two things worth doing:
- Fix the comment so it doesn't claim broadcasts never occur.
- Decide whether broadcast VID-share catchup must keep working post-cutover; if so it needs a real path through the coordinator's network, if not, a clearer/rate-limited error would beat a silent drop + timeout.
| .catchups(std::array::from_fn(|_| { | ||
| StatePeers::<SequencerApiVersion>::from_urls( | ||
| vec![format!("http://localhost:{api_port}").parse().unwrap()], | ||
| Default::default(), | ||
| Duration::from_secs(2), | ||
| &NoMetrics, |
There was a problem hiding this comment.
This test calls shut_down_legacy() directly, which is great for verifying the teardown itself. But it bypasses the production trigger entirely: the NewDecide counting, the == LEGACY_SHUTDOWN_DECIDE_COUNT threshold, and the detached spawn in handle_events (context.rs:644-653) are never exercised.
Consider either (a) a #[cfg(test)] override that lowers LEGACY_SHUTDOWN_DECIDE_COUNT so a test can drive real decides past the threshold and confirm the event-handler path fires exactly once, or (b) a note that the automatic trigger is covered elsewhere. As written, an off-by-one in the counter (== vs >=), a wrong event variant, or the spawn wiring would not be caught.
…t cutover With base version >= NEW_PROTOCOL_VERSION nothing can ever need the legacy stack: start_consensus tears it down instead of starting legacy consensus, and init_node no longer blocks boot waiting for CDN/libp2p readiness. The legacy network objects are still constructed (the production network type is fixed to CombinedNetworks) but are shut down before consensus starts. Upgraded networks (base < 0.6) keep the decide-count teardown window for peers still crossing the cutover boundary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Broadcast external messages have no path through the coordinator's network after the cutover, so post-cutover VID-share fetches were silently dropped and always came back empty. Batched requests are direct external messages, which the coordinator path carries, and they reach up to 100 peers within the request's 40s window (5 every 2s), covering the full committee. This was the last RequestType::Broadcast user. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop comments that restate adjacent code or logs, merge the two overlapping outbound-loop comments, remove caller context from the generic hotshot teardown doc, and correct the post-cutover drop-arm comment (all request-response traffic is batched direct messages now). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Successfully created backport PR for |
…r cutover decides (#4720) feat(new-protocol): tear down legacy stack after cutover decides (#4714) * feat(new-protocol): tear down legacy stack after cutover decides After LEGACY_SHUTDOWN_DECIDE_COUNT new-protocol decides, abort the legacy-event forwarders and shut down the legacy consensus tasks and network, leaving the shared membership coordinator (in-flight DRB computations) and in-memory legacy state intact. Outbound external messages switch to the coordinator's network once it is running. They are framed in the same versioned Message envelope as the legacy path, since the coordinator sends external payloads over the wire verbatim and the receiver's fallback decoder expects that framing; sending bare payload bytes made peers decode garbage lengths and abort on exabyte-scale allocations. On legacy network shutdown, release the combined network's dedup cache and per-view channels, which stay pinned after the cutover. * feat(new-protocol): don't run legacy stack when base version starts at cutover With base version >= NEW_PROTOCOL_VERSION nothing can ever need the legacy stack: start_consensus tears it down instead of starting legacy consensus, and init_node no longer blocks boot waiting for CDN/libp2p readiness. The legacy network objects are still constructed (the production network type is fixed to CombinedNetworks) but are shut down before consensus starts. Upgraded networks (base < 0.6) keep the decide-count teardown window for peers still crossing the cutover boundary. * fix(node): fetch VID shares via batched requests, not broadcast Broadcast external messages have no path through the coordinator's network after the cutover, so post-cutover VID-share fetches were silently dropped and always came back empty. Batched requests are direct external messages, which the coordinator path carries, and they reach up to 100 peers within the request's 40s window (5 every 2s), covering the full committee. This was the last RequestType::Broadcast user. * docs(node): trim comments from legacy-shutdown change Drop comments that restate adjacent code or logs, merge the two overlapping outbound-loop comments, remove caller context from the generic hotshot teardown doc, and correct the post-cutover drop-arm comment (all request-response traffic is batched direct messages now). * review comments --------- (cherry picked from commit 27bd573) Co-authored-by: Brendon Fish <bfish713@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Tear down the legacy consensus stack after cutover
Once the coordinator has emitted
LEGACY_SHUTDOWN_DECIDE_COUNT(100) decide events,handle_eventstriggersConsensusHandle::shut_down_legacy(): the legacy→coordinator forwarders are aborted and the legacy tasks + network are shut down via a newSystemContextHandle::shut_down_tasks_and_network(), which — unlikeshut_down()— leaves in-flight DRB computations on the shared membership coordinator running and legacy in-memory state readable.CombinedNetworks::shut_downnow also drops its dedup caches and per-view channels since the network object stays alive after the cutover.When the base version already starts at the cutover (≥ 0.6), the legacy stack never runs at all:
start_consensustears it down instead of starting legacy consensus, andinit_nodeno longer blocks boot waiting for CDN/libp2p readiness (a pure-0.6 network needs no legacy infra deployed). The legacy network objects are still constructed — the production network type is fixed toCombinedNetworks— but are shut down before consensus starts. Upgraded networks (base < 0.6) keep the 100-decide window so lagging peers can finish crossing the boundary.Outbound external (request-response) messages permanently switch to the coordinator's network once it's running. They're framed in the same versioned
Messageenvelope as the legacy path because the coordinator's network sends external payloads verbatim and the receiver's fallback decoder expects that framing — bare payload bytes made receivers parse garbage lengths and abort on exabyte-scale allocations (caught by the new test). VID-share fetches (request_vid_shares) switch fromRequestType::BroadcasttoRequestType::Batched— the last broadcast user — because the coordinator path only carries direct external messages; batched requests reach up to 100 peers within the request's 40s window (5 every 2s), covering the full committee.Test:
test_new_protocol_survives_legacy_shutdownruns a base-0.6 network (legacy stack torn down at startup on every node), re-issuesshut_down_legacy()mid-run to cover the decide-count trigger path being a harmless second teardown, and verifies decides continue across two epoch boundaries.Review focus: the external-message framing in
send_via_coordinator, and whether aborting the three forwarder tasks at teardown can race with a peer still crossing the cutover boundary.Follow-ups tracked separately:
versions::decode/ark-serialize preallocate lengths read off the wire with no bound, so any authenticated peer can abort a process with malformed bytes (pre-existing).handle_eventsis not driven end-to-end by a test (the test callsshut_down_legacy()directly); a#[cfg(test)]-overridable threshold would allow that if we want the coverage.🤖 Generated with Claude Code