Description
Description
Once a SIGTERM is issued, the host should be allowed the opportunity to gracefully drain and shutdown all existing http and WebSocket connections.
This used to work properly on netcore2.2, however we observed that since netcore3.1 this functionality is broken for WebSockets. This also remains broken on net5.0.
To Reproduce
Fully baked samples for netcore2.2, netcore3.1 and net5.0 can be found here.
https://github.com/RaviPidaparthi/PlayGround/tree/master/WebsocketGracefulShutdown
To repro
- Open, build and Start the solution from VS (>= 16.8.0 Preview version)
- This should start the client and 3 separate services each for netcore2.2, netcore3.1 and net5.0.
- In the client console window, press any key to start off the WebSocket connections to all the 3 services
- Wait for a few seconds until a few messages are sent to all 3 services.
- Press ctrl+c on all the 3 services
For netcore2.2 service, the the WebSocket will continue processing/reading messages until 30sec after shutdown is initiated.
For netcore3.1 and net5.0 services, the WebSocket read operation throws with below error as soon as shutdown is initiated.
Error=System.Threading.Tasks.TaskCanceledException: The request was aborted ---> Microsoft.AspNetCore.Connections.ConnectionAbortedException: The connection was aborted because the server is shutting down and request processing didn't complete within the time specified by HostOptions.ShutdownTimeout.
at System.IO.Pipelines.PipeCompletion.ThrowLatchedException()
at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
at System.IO.Pipelines.Pipe.GetReadAsyncResult()
at System.IO.Pipelines.Pipe.DefaultPipeReader.GetResult(Int16 token)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Http1MessageBody.PumpAsync()
at System.IO.Pipelines.PipeCompletion.ThrowLatchedException()
at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
at System.IO.Pipelines.Pipe.ReadAsync(CancellationToken token)
at System.IO.Pipelines.Pipe.DefaultPipeReader.ReadAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.StartTimingReadAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.ReadAsyncInternal(Memory`1 buffer, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.ReadAsyncInternal(Memory`1 buffer, CancellationToken cancellationToken)
at System.Net.WebSockets.ManagedWebSocket.EnsureBufferContainsAsync(Int32 minimumRequiredBytes, Boolean throwOnPrematureClosure)
at System.Net.WebSockets.ManagedWebSocket.ReceiveAsyncPrivate[TWebSocketReceiveResultGetter,TWebSocketReceiveResult](Memory`1 payloadBuffer, CancellationToken cancellationToken, TWebSocketReceiveResultGetter resultGetter)
at WebsocketGracefulShutdown22.Controllers.TestController.TestWsAsync(CancellationToken cancellationToken) in C:\Users\rapida\source\repos\WebsocketGracefulShutdown\WebsocketGracefulShutdown22\Controllers\TestController.cs:line 25
Code snippets
Configure shutdown timeout
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseShutdownTimeout(TimeSpan.FromSeconds(30)).UseStartup<Startup>();
});
Standup a Websocket endpoint
[HttpGetAttribute("/ws/test")]
public async Task TestWsAsync(CancellationToken cancellationToken)
{
var webSocket = await this.HttpContext.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
while (true)
{
try
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(new byte[64000], 0, 64000), cancellationToken).ConfigureAwait(false);
await Task.Delay(TimeSpan.FromMilliseconds(1000)).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.WriteLine(ex);
break;
}
}
}
Create a client
try
{
var websocket = new ClientWebSocket();
await websocket.ConnectAsync(new Uri($"ws://localhost:10050/ws/test"), CancellationToken.None).ConfigureAwait(false);
while (true)
{
var bytes = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString());
await websocket.SendAsync(new ArraySegment<byte>(bytes, 0, bytes.Length), WebSocketMessageType.Binary, true, CancellationToken.None).ConfigureAwait(false);
await Task.Delay(1000).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Exceptions
Error=System.Threading.Tasks.TaskCanceledException: The request was aborted ---> Microsoft.AspNetCore.Connections.ConnectionAbortedException: The connection was aborted because the server is shutting down and request processing didn't complete within the time specified by HostOptions.ShutdownTimeout.
at System.IO.Pipelines.PipeCompletion.ThrowLatchedException()
at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
at System.IO.Pipelines.Pipe.GetReadAsyncResult()
at System.IO.Pipelines.Pipe.DefaultPipeReader.GetResult(Int16 token)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Http1MessageBody.PumpAsync()
at System.IO.Pipelines.PipeCompletion.ThrowLatchedException()
at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
at System.IO.Pipelines.Pipe.ReadAsync(CancellationToken token)
at System.IO.Pipelines.Pipe.DefaultPipeReader.ReadAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.StartTimingReadAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.ReadAsync(Memory`1 buffer, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.ReadAsyncInternal(Memory`1 buffer, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.ReadAsyncInternal(Memory`1 buffer, CancellationToken cancellationToken)
at System.Net.WebSockets.ManagedWebSocket.EnsureBufferContainsAsync(Int32 minimumRequiredBytes, Boolean throwOnPrematureClosure)
at System.Net.WebSockets.ManagedWebSocket.ReceiveAsyncPrivate[TWebSocketReceiveResultGetter,TWebSocketReceiveResult](Memory`1 payloadBuffer, CancellationToken cancellationToken, TWebSocketReceiveResultGetter resultGetter)
at WebsocketGracefulShutdown22.Controllers.TestController.TestWsAsync(CancellationToken cancellationToken) in C:\Users\rapida\source\repos\WebsocketGracefulShutdown\WebsocketGracefulShutdown22\Controllers\TestController.cs:line 25
Further technical details
ASP.NET Core version=net5.0
Visual Studio Version 16.8.0 Preview 3.2