Repository navigation
GH-5254 Shutdown waits for handlers still running on a buffered local queue - #5265
Merged
Merged
Conversation
… queue BufferedReceiver.DrainAsync() only waits for in-flight messages when the receiver is already latched: unlatched, it assumes it was called from inside the handler pipeline, where waiting would deadlock. EndpointCollection.DrainAsync() drains local queues without latching them, and DurableLocalQueue latches its own receiver but BufferedLocalQueue never did. So IHost.StopAsync() returned while a handler on a buffered local queue was still running, and the host disposed its logging and container under it. That is the metrics_source_tag failure on Windows: the TrackedSession completes on the MovedToErrorQueue record, which WolverineRuntime.MovedToErrorQueue() writes just BEFORE it logs, so the test tears the host down while that log call is still to come, and the EventLog provider throws ObjectDisposedException. The recovery path's logger is disposed too, hence the follow-on "could not recover from a cascading failure". Shutdown now latches each buffered local queue before draining it, so the drain waits (bounded by DurabilitySettings.DrainTimeout) for in-flight handlers, as it already does for durable local queues and for listeners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U
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.
Fixes #5254.
Cause
The owner is runtime shutdown, not the EventLog provider or overlapping test hosts.
BufferedReceiver.DrainAsync()only waits for in-flight messages when the receiver is already latched (var waitForCompletion = _latched;). If it isn't latched, it assumes it was called from inside the handler pipeline, where waiting would deadlock.EndpointCollection.DrainAsync()drains local queues without latching them.DurableLocalQueue.DrainAsync()latches its own receiver first;BufferedLocalQueue, which inheritsBufferedReceiver.DrainAsync(), never did. SoIHost.StopAsync()returned while a handler on a buffered local queue was still running, and the host then disposed its logging and container under it.In
metrics_source_tag, theMetricFailtracked session completes on theMovedToErrorQueuerecord.WolverineRuntime.MovedToErrorQueue()writes that record just before it logs the move (WolverineRuntime.Tracking.cs). The test therefore resumes and tears the host down while that log call is still to come, and on Windows the default EventLog provider throwsObjectDisposedException. The recovery path's logger is disposed as well, which produces the follow-on "could not recover from a cascading failure".Evidence: the new regression test holds a handler at a gate on a buffered local queue and calls
StopAsync(). Onmain,StopAsync()completes while the handler is still held (stopping.IsCompletedis true after 250 ms). With the fix it waits.Change
EndpointCollection.DrainAsync()latches eachBufferedLocalQueuebefore draining it. The drain then waits for in-flight handlers, bounded byDurabilitySettings.DrainTimeout, as it already does for durable local queues and for listeners.The in-pipeline drain path (pausing) is untouched. It doesn't go through
EndpointCollection.DrainAsync(), so the deadlock guard inBufferedReceiver.DrainAsync()still holds there.No assertions are weakened, and no
ObjectDisposedExceptionis swallowed.metrics_source_tagis unchanged: the intentional handler failure and the metrics assertions stay as they were.Tests
CoreTests/Runtime/buffered_local_queue_shutdown_waits_for_in_flight_5254.cs. A gated handler on a buffered local queue fails and moves to the error queue, likeMetricFail. A logger provider records anything written to it after it has been disposed, standing in for EventLog on macOS and Linux. The test asserts that:StopAsync()does not complete while the handler is held;It fails on
mainand passes with the fix.Local verification (macOS, net9.0, Release; Windows not run locally):
dotnet build wolverine.slnx -c Release -f net9.0: cleanmetrics_source_tag)For review: a host whose buffered-queue handler never returns now waits up to
DrainTimeout(30 s by default) at shutdown, instead of stopping out from under the handler. That is the behaviour durable local queues and listeners already had.🤖 Generated with Claude Code
https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U