Repository navigation
Review fixes for #115: shutdown order, mux slot retirement, and runtime edge cases - #118
Merged
jthomson04 merged 29 commits intoOct 5, 2026
Merged
Conversation
…peaks HTTP/2 tonic's graceful shutdown waits for every connection, and a connection that never finished its HTTP/2 handshake cannot receive GOAWAY. A health check or a half-open peer therefore held GrpcFrameTransport::shutdown, and with it Velo::shutdown, open forever. Drop the server after a short grace period so the listener closes and shutdown returns. Also move a regression doc comment back onto the test it describes. Signed-off-by: Ryan Olson <rolson@nvidia.com>
detach now re-arms the anchor's unattached timeout once the Detached sentinel is queued. On the fast path that runs on the caller's thread, and a thread with no runtime (a sender moved to a binding's thread) could not spawn the timer, so an anchor nobody re-attached was never reaped. Enter the sender's runtime there. Signed-off-by: Ryan Olson <rolson@nvidia.com>
detach always spawned its terminal send on the runtime the sender was attached on. Once that runtime stopped, the task was dropped, detach returned ChannelClosed with room in the channel, and the consumer saw neither Detached nor Dropped. Queue the terminal with try_send first, which commits it at once, and only on a full channel spawn the wait on the caller's runtime. Drop and the cleanup guard follow the same rule. Signed-off-by: Ryan Olson <rolson@nvidia.com>
CancellationToken::is_cancelled takes a mutex, and every peer and lane of a mux checks StreamTasks::is_stopped several times per batch. Keep an AtomicBool next to the token, set under the admission lock in stop(). A microbenchmark of the check alone: 10.8 ns uncontended and 203 ns with 4 threads for the token, 0.5 ns and 1.1 ns for the flag. Signed-off-by: Ryan Olson <rolson@nvidia.com>
MuxCore carried a cancel token that was only another name for its StreamTasks token; read the tasks instead. Stopping the tasks and closing ingress happened in four places; one stop_mux helper does it now. Batcher start and the stop path move to messenger_mux/lifecycle.rs, which brings mod.rs back under 1000 lines. The batcher keeps its own cancel handle, now documented: a stop that lands while its loop is polled exits through teardown(true), and tests use a separate token to drive that exit. Signed-off-by: Ryan Olson <rolson@nvidia.com>
StartupTransports::stop and VeloBackend::shutdown_now repeated the same drain, teardown and shutdown steps. Both drain calls are idempotent, so one helper serves both. Signed-off-by: Ryan Olson <rolson@nvidia.com>
Four places sent an error response and logged a failure unless it was an admission refusal. They now share send_error_reply. fail_fast awaits the reply in the handler task, and the ordered-lane shed path spawns it on the messenger task tracker instead of an untracked task. Signed-off-by: Ryan Olson <rolson@nvidia.com>
ConnHandle held a channel sender only so the pump could recognise its own epoch on cleanup. The handle is cloned on every send, so that cost a shared atomic inc and dec each time. The admission Arc is already in the handle and is unique per epoch, so compare that instead. Signed-off-by: Ryan Olson <rolson@nvidia.com>
Velo::shutdown tears down the messenger transports before the anchor manager. Removing an anchor with a live mux stream closes its slot, and while the mux still ran that close was batched to the peer: TCP had no connection left, dialed, found itself cancelled, and failed the frame as a send error logged at ERROR. Stop the mux first, so the close finds no batcher and returns. The new test failed in 2 of 3 runs before the change and passed 20 of 20 after it. Signed-off-by: Ryan Olson <rolson@nvidia.com>
Signed-off-by: Ryan Olson <rolson@nvidia.com>
A stream sender dropped from a thread-local destructor can outlive the thread's tokio context, and Handle::enter panics on a destroyed context; a panic there aborts the process. Enter the sender's runtime only when the thread has no runtime at all, through one helper both senders use. Otherwise the timer is not armed, as with no runtime. Signed-off-by: Ryan Olson <rolson@nvidia.com>
Moving the shed reply into a tracked task also moved the fire-and-forget check inside it, so every shed fire-and-forget message spawned a task and allocated the handler name just to return. Check before spawning: shedding is the overload path and must stay cheaper than accepting. Signed-off-by: Ryan Olson <rolson@nvidia.com>
Stopping the mux first kept shutdown from writing to torn-down transports, but stopping it retires every ingress slot by injecting Dropped. A consumer still reading through its direct feed took that as its sender's and ended with SenderDropped. Withdraw every anchor's feed first, as AnchorEntry::drop does before it closes a slot; the consumer then sees its stream end cleanly. Failed 5 of 5 runs before, passes 10 of 10 after. Signed-off-by: Ryan Olson <rolson@nvidia.com>
When the last local MPSC sender leaves, its anchor re-arms the unattached timeout with a bare tokio::spawn. From a thread-local destructor, after tokio's context is gone, that panics, and a panic there aborts the process. Guard it with Handle::try_current like the SPSC timeout; with no runtime the timer does not fire. Signed-off-by: Ryan Olson <rolson@nvidia.com>
Withdrawing SPSC feeds before the mux stops did not cover MPSC, whose slot records reach the consumer through a pump: the pump forwarded the Dropped that slot retirement injects, as the sender's own. Cancelling the pumps first is not enough on its own, since a stopped pump releases its slot, and a running mux would send that close over the torn-down transport. So shutdown stops the mux's tasks first (nothing more is written to a peer), then takes streams off their slots (SPSC feeds withdrawn, MPSC pumps cancelled), then retires the slots. Signed-off-by: Ryan Olson <rolson@nvidia.com>
Signed-off-by: Ryan Olson <rolson@nvidia.com>
Shutdown cancels a sender on the same instance: its cancellation token fires and later sends fail. The reader ends when the application drops or finalizes that sender; shutdown does not end the reader itself, because that needs a check on every read. A test pins this for SPSC and MPSC streams. Signed-off-by: Ryan Olson <rolson@nvidia.com>
Shutdown stops the mux's tasks, takes every stream off its slot, then retires the slots. But a batcher parked at an await outside its cancel arm is dropped by its task wrapper when the stop lands, and its Drop retired every slot at once, so a reader still on its slot could take the injected Dropped as its sender's. Only the first stop of a failed mux retires slots now; shutdown and MuxCore::drop retire them explicitly. deliver_batch re-closes a late claim once the slots are retired, keyed on a flag shutdown sets before retiring, not on the tasks having stopped. Signed-off-by: Ryan Olson <rolson@nvidia.com>
…wait AnchorManager::shutdown withdrew feeds before it awaited the mux's tasks but removed anchors only after. A caller that drops the future during that await (a timeout around Velo::shutdown) left anchors with no feed in the registry, and their readers waiting. Remove anchors, cancel MPSC entries and local senders before the await; the mux is already stopped, so none of it sends. Document the phases and the two rules they keep, and update the mux lifecycle module doc to match. Signed-off-by: Ryan Olson <rolson@nvidia.com>
The messenger, its server and its dispatcher hold each other, so dropping the last handle does not free the instance. The book already says shutdown does not release the ownership graph; the README and the Velo::shutdown rustdoc now agree with it. Signed-off-by: Ryan Olson <rolson@nvidia.com>
IngressRegistry::shutdown walked the slot tables and only then cleared the binds. A batch handler claiming a bind between the two put a live slot in a table already walked, and that slot was never retired; its reader waited forever. The last change made this reachable on the failed-mux path too, where deliver_batch no longer re-closed the claim. Clear binds first. A claim takes its bind under the peer table's lock, in a table it inserted before the claim, so it either finds no bind or leaves a slot the walk still retires. The slots_retired flag and the late-claim re-close in deliver_batch are no longer needed. Signed-off-by: Ryan Olson <rolson@nvidia.com>
…s the mux The mux runs until AnchorManager::shutdown stops it, which is after graceful_shutdown has torn the transports down; its batchers can hand batches to a transport that is going away in that window. Say so rather than claim nothing is sent. Signed-off-by: Ryan Olson <rolson@nvidia.com>
Signed-off-by: Ryan Olson <rolson@nvidia.com>
Three shutdown tests each built the same TCP node and the same two-peer setup inline. Move that into two helpers in the test module. The tests keep their assertions, timeouts and comments. Signed-off-by: Ryan Olson <rolson@nvidia.com>
…nicking prebind_anchor binds a mux slot and then takes the drain signal the bind parked. Mux shutdown clears parked drain signals, so a public, sync prebind_anchor call on another thread could find it gone and panic on an expect. Release the bind and return None, as a pre-bind that finds the mux stopped does. Signed-off-by: Ryan Olson <rolson@nvidia.com>
…el is full A consumer that holds its anchor without reading fills the anchor channel, and the reader pump then waited on the send without watching its cancel token. Velo::shutdown cancels the pump and removes the anchor, but the application still holds the receiver, so the pump kept its transport receiver and channel alive. Race the send against the cancel on the full-channel path, as the MPSC pump already does; the try_send fast path is unchanged. Also say in the Velo::shutdown doc that reader pumps are cancelled, not joined. Signed-off-by: Ryan Olson <rolson@nvidia.com>
…bort Nothing aborts a batcher on purpose: one that ends without its teardown has panicked, which tokio reports, or its runtime shut down before Velo::shutdown ran. The ERROR claiming an unexpected abort misled the second case. Log a warning that names both causes. Signed-off-by: Ryan Olson <rolson@nvidia.com>
spawn_until_done returns a refused future with the admission lock released only because a parameter drops after the body's locals. A batcher's Drop stops the service and takes that lock again, so a change that dropped the future under the lock would deadlock. Say so at the return, and add a test whose refused future stops the service from its Drop; dropping it under the lock fails the test. Signed-off-by: Ryan Olson <rolson@nvidia.com>
The two exits taken when the mux is stopped returned None without the debug line and the error count every other failed pre-bind records, so an operator could not tell a shut-down mux from zero-RTT being off. Record and log them like the rest. Signed-off-by: Ryan Olson <rolson@nvidia.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
ryanolson
marked this pull request as ready for review
October 5, 2026 16:11
jthomson04
merged commit Oct 5, 2026
6896e91
into
jthomson04/velo-reliability-cleanup
3 checks passed
This was referenced Oct 5, 2026
jthomson04
pushed a commit
that referenced
this pull request
Oct 5, 2026
Preserve shutdown ordering, retire mux slots safely, and handle stream cleanup across runtime shutdown. Consolidate error replies and transport teardown. Signed-off-by: jthomson04 <jwillthomson19@gmail.com> Signed-off-by: Ryan Olson <rolson@nvidia.com>
ryanolson
added a commit
that referenced
this pull request
Oct 6, 2026
* fix(transports): complete queued sends and close failed startup Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * fix(runtime): close owned tasks and preserve stream lifecycles Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * chore(build): trim grpc dependencies and test core configurations Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * test(soak): use explicit drain and bounded response waits Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * fix(velo): stop mux retries when Tokio drops a batcher Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * fix(transports): retire accepted sends and roll back partial startup Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * fix(runtime): preserve ownership and close stream lifecycle races Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * ci(velo): check core libraries without dev dependency features Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * fix(streaming): retire inbound claims that race mux shutdown Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * test(mux): explain retirement regression timing Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * fix(runtime): preserve shutdown ordering and stream cleanup (#118) Preserve shutdown ordering, retire mux slots safely, and handle stream cleanup across runtime shutdown. Consolidate error replies and transport teardown. Signed-off-by: jthomson04 <jwillthomson19@gmail.com> Signed-off-by: Ryan Olson <rolson@nvidia.com> * fix(runtime): close accepted gRPC sockets during shutdown Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * ci: reduce test linker disk use Signed-off-by: jthomson04 <jwillthomson19@gmail.com> * fix(runtime): join mux sends before transport teardown Signed-off-by: jthomson04 <jwillthomson19@gmail.com> --------- Signed-off-by: jthomson04 <jwillthomson19@gmail.com> Signed-off-by: Ryan Olson <rolson@nvidia.com> Co-authored-by: Ryan Olson <rolson@nvidia.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds 29 commits on top of the head of #115 (
f3e0bbe). They come from an 11-pass code review. Each pass had a strict review and an independent pass by a second model. The last pass found no actionable findings. Each correctness fix has a test that failed before the change.Shutdown
The gRPC stream server shutdown waited forever for a connection that never sent the HTTP/2 preface. Now the server drops after a 1 s grace period.
AnchorManager::shutdownruns in this order:Steps 1 to 3 do not await. Before this change, three failures were possible:
SenderDropped.send_errorwas logged at ERROR.IngressRegistry::shutdownclears binds before it walks the slot tables. A concurrent claim cannot leak a slot.stop_muxretires slots only on the first stop.Runtime edge cases
detachfailed after the runtime that made the sender stopped. Now it triestry_sendfirst, then it uses the runtime of the caller.Handle::enterand fromtokio::spawnon a destroyed context.prebind_anchorno longer panics when it races shutdown. It counts that case as a failed pre-bind.Performance
StreamTasks::is_stoppedreads anAtomicBool.CancellationToken::is_cancelled, which it used before, takes a mutex. A microbenchmark of the check alone:is_cancelledAtomicBoolUCX no longer clones a flume
Senderfor each send.Structure
messenger_mux/lifecycle.rs.mod.rsis now less than 1000 lines.Documentation
The
Velo::shutdownrustdoc,README.md, anddocs/src/concepts/shutdown.mdnow state two facts:Validation
cargo test --all-features --all-targets: 1,751 passed, 0 failed, 2 ignored. NATS and etcd were running. UCX ran withUCX_TLS=tcp.cargo clippy --all-features --no-deps --all-targets -- -D warningsandcargo fmt --checkare clean.scripts/check-semver.shpasses:velo0.18.1 against 0.18.0, andvelo-ext0.5.4.Deferred to #116
These items are documented in the code where relevant:
graceful_shutdowntears down the transports.Messengeris dropped without shutdown.Messenger, its server, and its hub hold references to each other.AnchorManagerhas no admission gate after shutdown.Notes
f3e0bbe. After this PR merges, they need a rebase onto fix(runtime): complete sends and release resources on shutdown #115. Several of these fixes change the shutdown code that they rework.