Skip to content

WebSocket does not honor ShutDownTimeout once SIGTERM/Ctrl+C is issued #26482

Closed
@RaviPidaparthi

Description

@RaviPidaparthi

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

  1. Open, build and Start the solution from VS (>= 16.8.0 Preview version)
  2. This should start the client and 3 separate services each for netcore2.2, netcore3.1 and net5.0.
  3. In the client console window, press any key to start off the WebSocket connections to all the 3 services
  4. Wait for a few seconds until a few messages are sent to all 3 services.
  5. 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

Metadata

Metadata

Assignees

Labels

area-networkingIncludes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions