File: src\Servers\Kestrel\test\FunctionalTests\Http2\ShutdownTests.cs
Web Access
Project: src\src\Servers\Kestrel\test\Sockets.FunctionalTests\Sockets.FunctionalTests.csproj (Sockets.FunctionalTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
 
#if SOCKETS
namespace Microsoft.AspNetCore.Server.Kestrel.Sockets.FunctionalTests.Http2;
#else
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2;
#endif
 
[TlsAlpnSupported]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public class ShutdownTests : TestApplicationErrorLoggerLoggedTest
{
    private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate();
 
    private HttpClient Client { get; set; }
    private List<Http2Frame> ReceivedFrames { get; } = new List<Http2Frame>();
 
    public ShutdownTests()
    {
        var handler = new SocketsHttpHandler
        {
            KeepAlivePingDelay = TimeSpan.MaxValue,
        };
        handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true;
        Client = new HttpClient(handler)
        {
            DefaultRequestVersion = new Version(2, 0),
        };
    }
 
    [ConditionalFact]
    public async Task ConnectionClosedWithoutActiveRequestsOrGoAwayFIN()
    {
        var connectionClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var readFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var writeFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
        TestSink.MessageLogged += context =>
        {
 
            if (context.EventId.Name == "Http2ConnectionClosed")
            {
                connectionClosed.SetResult();
            }
            else if (context.EventId.Name == "ConnectionReadFin")
            {
                readFin.SetResult();
            }
            else if (context.EventId.Name == "ConnectionWriteFin")
            {
                writeFin.SetResult();
            }
        };
 
        var testContext = new TestServiceContext(LoggerFactory);
 
        testContext.InitializeHeartbeat();
 
        await using (var server = new TestServer(context =>
        {
            return context.Response.WriteAsync("hello world " + context.Request.Protocol);
        },
        testContext,
        kestrelOptions =>
        {
            kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions =>
            {
                listenOptions.Protocols = HttpProtocols.Http2;
                listenOptions.UseHttps(_x509Certificate2);
            });
        }))
        {
            // HttpClient sends PING frames even if you disable them so that it can dynamically adjust the HTTP/2 window size.
            // It sends 4 PINGs to do this, and they sent after receiving data, so we send and receive 5 times to make sure the PINGs are done.
            // https://github.com/dotnet/runtime/blob/a590cb4cfb9f1a66c043476695fd0e79835842eb/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2StreamWindowManager.cs#L165
            // We care because responding with a PING ack when the client is disposing can cause a ConnectionReset log instead of ConnectionReadFin
            // which would hang the test.
            for (var i = 0; i < 5; i++)
            {
                var response = await Client.GetStringAsync($"https://localhost:{server.Port}/");
                Assert.Equal("hello world HTTP/2", response);
            }
            Client.Dispose(); // Close the socket, no GoAway is sent.
 
            await readFin.Task.DefaultTimeout();
            await writeFin.Task.DefaultTimeout();
            await connectionClosed.Task.DefaultTimeout();
 
            await server.StopAsync();
        }
    }
 
    [CollectDump]
    [ConditionalFact]
    public async Task GracefulShutdownWaitsForRequestsToFinish()
    {
        var requestStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var requestUnblocked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var requestStopping = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
        TestSink.MessageLogged += context =>
        {
 
            if (context.EventId.Name == "Http2ConnectionClosing")
            {
                requestStopping.SetResult();
            }
        };
 
        var testContext = new TestServiceContext(LoggerFactory);
 
        testContext.InitializeHeartbeat();
 
        await using (var server = new TestServer(async context =>
        {
            requestStarted.SetResult();
            await requestUnblocked.Task.DefaultTimeout();
            await context.Response.WriteAsync("hello world " + context.Request.Protocol);
        },
        testContext,
        kestrelOptions =>
        {
            kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions =>
            {
                listenOptions.Protocols = HttpProtocols.Http2;
                listenOptions.UseHttps(_x509Certificate2);
            });
        }))
        {
            var requestTask = Client.GetStringAsync($"https://localhost:{server.Port}/");
            Assert.False(requestTask.IsCompleted);
 
            await requestStarted.Task.DefaultTimeout();
 
            var stopTask = server.StopAsync();
 
            await requestStopping.Task.DefaultTimeout();
 
            // Unblock the request
            requestUnblocked.SetResult();
 
            Assert.Equal("hello world HTTP/2", await requestTask);
            await stopTask.DefaultTimeout();
        }
 
        Assert.Contains(LogMessages, m => m.Message.Contains("Request finished "));
        Assert.Contains(LogMessages, m => m.Message.Contains("is closing."));
        Assert.Contains(LogMessages, m => m.Message.Contains("is closed. The last processed stream ID was 1."));
    }
 
    [ConditionalFact]
    public async Task GracefulTurnsAbortiveIfRequestsDoNotFinish()
    {
        var requestStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var requestUnblocked = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
 
        var testContext = new TestServiceContext(LoggerFactory)
        {
            MemoryPoolFactory = () => new PinnedBlockMemoryPool()
        };
 
        ThrowOnUngracefulShutdown = false;
 
        // Abortive shutdown leaves one request hanging
        await using (var server = new TestServer(async context =>
        {
            requestStarted.SetResult();
            await requestUnblocked.Task.DefaultTimeout();
            await context.Response.WriteAsync("hello world " + context.Request.Protocol);
        },
        testContext,
        kestrelOptions =>
        {
            kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions =>
            {
                listenOptions.Protocols = HttpProtocols.Http2;
                listenOptions.UseHttps(_x509Certificate2);
            });
        },
        _ => { }))
        {
            var requestTask = Client.GetStringAsync($"https://localhost:{server.Port}/");
            Assert.False(requestTask.IsCompleted);
            await requestStarted.Task.DefaultTimeout();
 
            // Wait for the graceful shutdown log before canceling the token passed to StopAsync and triggering an ungraceful shutdown.
            // Otherwise, graceful shutdown might be skipped causing there to be no corresponding log. https://github.com/dotnet/aspnetcore/issues/6556
            var closingMessageTask = WaitForLogMessage(m => m.Message.Contains("is closing.")).DefaultTimeout();
 
            var cts = new CancellationTokenSource();
            var stopServerTask = server.StopAsync(cts.Token).DefaultTimeout();
 
            await closingMessageTask;
 
            var closedMessageTask = WaitForLogMessage(m => m.Message.Contains("is closed. The last processed stream ID was 1.")).DefaultTimeout();
            cts.Cancel();
 
            // Wait for "is closed" message as this is logged from a different thread and aborting
            // can timeout and return from server.StopAsync before this is logged.
            await closedMessageTask;
            requestUnblocked.SetResult();
            await stopServerTask;
        }
 
        Assert.Contains(LogMessages, m => m.Message.Contains("is closing."));
        Assert.Contains(LogMessages, m => m.Message.Contains("is closed. The last processed stream ID was 1."));
        Assert.Contains(LogMessages, m => m.Message.Contains("Some connections failed to close gracefully during server shutdown."));
        Assert.DoesNotContain(LogMessages, m => m.Message.Contains("Request finished in"));
    }
}