Stop the Blazor admin UI blocking a graceful shutdown - #5693
Stop the Blazor admin UI blocking a graceful shutdown#5693timothycoleman wants to merge 2 commits into
Conversation
PR Summary by QodoPrevent Blazor admin UI circuits from blocking graceful shutdown
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
There was a problem hiding this comment.
Pull request overview
This PR adds an ASP.NET Core middleware to prevent the embedded Blazor admin UI from re-establishing SignalR circuits during shutdown, which can otherwise keep Kestrel draining until HostOptions.ShutdownTimeout and force an ungraceful shutdown.
Changes:
- Registers a new middleware early in the pipeline (before
Startup.Configure) to intercept/_blazorrequests. - Implements
BlazorShutdownMiddlewareto return503onceApplicationStoppinghas fired and toAbort()in-flight circuit requests when shutdown begins. - Adds unit tests covering pass-through behavior, refusal during stopping, and aborting an in-flight circuit.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/KurrentDB/Program.cs | Registers BlazorShutdownMiddleware early to ensure /_blazor requests are intercepted before endpoint handling. |
| src/KurrentDB/BlazorShutdownMiddleware.cs | Introduces middleware that refuses new circuits during shutdown and aborts existing circuit requests when stopping starts. |
| src/KurrentDB.Components.Tests/BlazorShutdownMiddlewareTests.cs | Adds tests verifying the middleware behavior for non-circuit paths, circuit refusal during stopping, and aborting pre-shutdown circuits. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Code Review by Qodo
1.
|
On shutdown SignalR aborts the Blazor circuits it is tracking, and the client immediately reconnects over the HTTP/2 connection the browser already holds. Kestrel is closing that connection, but a stream whose headers were already in flight when GOAWAY went out is still processed, so the reconnect wins the race and a fresh transport request begins after SignalR has finished closing what it was tracking. Nothing then ends that request, so Kestrel's drain waits for it until HostOptions.ShutdownTimeout expires, followed by a forced shutdown. Refuse /_blazor requests with a 503 once ApplicationStopping has fired, and abort a circuit request that started just before the signal. Cancelling RequestAborted is not enough there - the transport ignores it and runs on until Kestrel resets the stream at the timeout. The middleware is registered ahead of Startup.Configure because routing and the endpoints are set up in there, and a circuit request would otherwise be handled by its endpoint before reaching the middleware. Workaround for dotnet/aspnetcore#58947, which is still open; the root cause is confirmed in dotnet/aspnetcore#58947 (comment) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3e6c9d7 to
8ca93dd
Compare
On shutdown SignalR aborts the Blazor circuits it is tracking, and the client immediately reconnects over the HTTP/2 connection the browser already holds. Kestrel is closing that connection, but a stream whose headers were already in flight when GOAWAY went out is still processed, so the reconnect wins the race and a fresh transport request begins after SignalR has finished closing what it was tracking. Nothing then ends that request, so Kestrel's drain waits for it until HostOptions.ShutdownTimeout expires, followed by a forced shutdown.
Refuse /_blazor requests with a 503 once ApplicationStopping has fired, and abort a circuit request that started just before the signal. Cancelling RequestAborted is not enough there - the transport ignores it and runs on until Kestrel resets the stream at the timeout.
The middleware is registered ahead of Startup.Configure because routing and the endpoints are set up in there, and a circuit request would otherwise be handled by its endpoint before reaching the middleware.
Workaround for dotnet/aspnetcore#58947, which is still open; the root cause is confirmed in
dotnet/aspnetcore#58947 (comment)