diff --git a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/application.h b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/application.h index add050b2082d..36ca2f073d81 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/application.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/CommonLib/application.h @@ -7,6 +7,7 @@ #include "iapplication.h" #include "ntassert.h" #include "SRWExclusiveLock.h" +#include "SRWSharedLock.h" #include "exceptions.h" class APPLICATION : public IAPPLICATION @@ -21,14 +22,17 @@ class APPLICATION : public IAPPLICATION _In_ IHttpContext *pHttpContext, _Outptr_result_maybenull_ IREQUEST_HANDLER **pRequestHandler) override { - TraceContextScope traceScope(pHttpContext->GetTraceContext()); *pRequestHandler = nullptr; + SRWSharedLock stopLock(m_stateLock); + if (m_fStopCalled) { return S_FALSE; } + TraceContextScope traceScope(pHttpContext->GetTraceContext()); + return CreateHandler(pHttpContext, pRequestHandler); } diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp index 0b27fd1fc356..af1c3e2594ef 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.cpp @@ -26,7 +26,8 @@ IN_PROCESS_APPLICATION::IN_PROCESS_APPLICATION( m_Initialized(false), m_blockManagedCallbacks(true), m_waitForShutdown(true), - m_pConfig(std::move(pConfig)) + m_pConfig(std::move(pConfig)), + m_requestCount(0) { DBG_ASSERT(m_pConfig); @@ -67,6 +68,13 @@ IN_PROCESS_APPLICATION::StopClr() { shutdownHandler(m_ShutdownHandlerContext); } + + auto requestCount = m_requestCount.load(); + if (requestCount == 0) + { + LOG_INFO(L"Drained all requests, notifying managed."); + m_RequestsDrainedHandler(m_ShutdownHandlerContext); + } } // Signal shutdown @@ -90,6 +98,7 @@ IN_PROCESS_APPLICATION::SetCallbackHandles( _In_ PFN_SHUTDOWN_HANDLER shutdown_handler, _In_ PFN_DISCONNECT_HANDLER disconnect_callback, _In_ PFN_ASYNC_COMPLETION_HANDLER async_completion_handler, + _In_ PFN_REQUESTS_DRAINED_HANDLER requestsDrainedHandler, _In_ VOID* pvRequstHandlerContext, _In_ VOID* pvShutdownHandlerContext ) @@ -102,6 +111,7 @@ IN_PROCESS_APPLICATION::SetCallbackHandles( m_ShutdownHandler = shutdown_handler; m_ShutdownHandlerContext = pvShutdownHandlerContext; m_AsyncCompletionHandler = async_completion_handler; + m_RequestsDrainedHandler = requestsDrainedHandler; m_blockManagedCallbacks = false; m_Initialized = true; @@ -131,6 +141,12 @@ IN_PROCESS_APPLICATION::LoadManagedApplication() FALSE, // not set nullptr)); // name + THROW_LAST_ERROR_IF_NULL(m_pRequestDrainEvent = CreateEvent( + nullptr, // default security attributes + TRUE, // manual reset event + FALSE, // not set + nullptr)); // name + LOG_INFO(L"Waiting for initialization"); m_workerThread = std::thread([](std::unique_ptr application) @@ -167,7 +183,6 @@ IN_PROCESS_APPLICATION::LoadManagedApplication() return S_OK; } - void IN_PROCESS_APPLICATION::ExecuteApplication() { @@ -259,7 +274,7 @@ IN_PROCESS_APPLICATION::ExecuteApplication() if (m_waitForShutdown) { const auto clrWaitResult = WaitForSingleObject(m_clrThread.native_handle(), m_pConfig->QueryShutdownTimeLimitInMS()); - THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); + THROW_LAST_ERROR_IF(clrWaitResult == WAIT_FAILED); clrThreadExited = clrWaitResult != WAIT_TIMEOUT; } @@ -517,9 +532,23 @@ IN_PROCESS_APPLICATION::CreateHandler( { try { + DBG_ASSERT(!m_fStopCalled); + m_requestCount++; *pRequestHandler = new IN_PROCESS_HANDLER(::ReferenceApplication(this), pHttpContext, m_RequestHandler, m_RequestHandlerContext, m_DisconnectHandler, m_AsyncCompletionHandler); } CATCH_RETURN(); return S_OK; } + +void +IN_PROCESS_APPLICATION::HandleRequestCompletion() +{ + SRWSharedLock lock(m_stateLock); + auto requestCount = m_requestCount--; + if (m_fStopCalled && requestCount == 0) + { + LOG_INFO(L"Drained all requests, notifying managed."); + m_RequestsDrainedHandler(m_ShutdownHandlerContext); + } +} diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h index 61eb6268228f..e99280280638 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocessapplication.h @@ -13,6 +13,7 @@ typedef REQUEST_NOTIFICATION_STATUS(WINAPI * PFN_REQUEST_HANDLER) (IN_PROCESS_HA typedef VOID(WINAPI * PFN_DISCONNECT_HANDLER) (void *pvManagedHttpContext); typedef BOOL(WINAPI * PFN_SHUTDOWN_HANDLER) (void* pvShutdownHandlerContext); typedef REQUEST_NOTIFICATION_STATUS(WINAPI * PFN_ASYNC_COMPLETION_HANDLER)(void *pvManagedHttpContext, HRESULT hrCompletionStatus, DWORD cbCompletion); +typedef void(WINAPI * PFN_REQUESTS_DRAINED_HANDLER) (void* pvShutdownHandlerContext); class IN_PROCESS_APPLICATION : public InProcessApplicationBase { @@ -36,6 +37,7 @@ class IN_PROCESS_APPLICATION : public InProcessApplicationBase _In_ PFN_SHUTDOWN_HANDLER shutdown_callback, _In_ PFN_DISCONNECT_HANDLER disconnect_callback, _In_ PFN_ASYNC_COMPLETION_HANDLER managed_context_callback, + _In_ PFN_REQUESTS_DRAINED_HANDLER requestsDrainedHandler, _In_ VOID* pvRequstHandlerContext, _In_ VOID* pvShutdownHandlerContext ); @@ -47,6 +49,8 @@ class IN_PROCESS_APPLICATION : public InProcessApplicationBase _Out_ IREQUEST_HANDLER **pRequestHandler) override; + void HandleRequestCompletion(); + // Executes the .NET Core process void ExecuteApplication(); @@ -62,6 +66,8 @@ class IN_PROCESS_APPLICATION : public InProcessApplicationBase StopIncomingRequests() { QueueStop(); + + LOG_INFOF(L"Waiting for %d requests to drain", m_requestCount.load()); } void @@ -144,6 +150,8 @@ class IN_PROCESS_APPLICATION : public InProcessApplicationBase // The event that gets triggered when worker thread should exit HandleWrapper m_pShutdownEvent; + HandleWrapper m_pRequestDrainEvent; + // The request handler callback from managed code PFN_REQUEST_HANDLER m_RequestHandler; VOID* m_RequestHandlerContext; @@ -154,12 +162,14 @@ class IN_PROCESS_APPLICATION : public InProcessApplicationBase PFN_ASYNC_COMPLETION_HANDLER m_AsyncCompletionHandler; PFN_DISCONNECT_HANDLER m_DisconnectHandler; + PFN_REQUESTS_DRAINED_HANDLER m_RequestsDrainedHandler; std::wstring m_dotnetExeKnownLocation; std::atomic_bool m_blockManagedCallbacks; bool m_Initialized; bool m_waitForShutdown; + std::atomic m_requestCount; std::unique_ptr m_pConfig; diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp index ec9e8f4f44e2..883e4ff49d51 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.cpp @@ -58,7 +58,6 @@ IN_PROCESS_HANDLER::AsyncCompletion( HRESULT hrCompletionStatus ) { - ::RaiseEvent(m_pW3Context, nullptr); if (m_fManagedRequestComplete) diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h index 20a5192c18cc..faa446968540 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/inprocesshandler.h @@ -21,7 +21,10 @@ class IN_PROCESS_HANDLER : public REQUEST_HANDLER _In_ PFN_DISCONNECT_HANDLER m_DisconnectHandler, _In_ PFN_ASYNC_COMPLETION_HANDLER pAsyncCompletion); - ~IN_PROCESS_HANDLER() override = default; + ~IN_PROCESS_HANDLER() + { + m_pApplication->HandleRequestCompletion(); + } __override REQUEST_NOTIFICATION_STATUS diff --git a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp index 6f52892b6fa5..93c9d6a97bc6 100644 --- a/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp +++ b/src/Servers/IIS/AspNetCoreModuleV2/InProcessRequestHandler/managedexports.cpp @@ -18,6 +18,7 @@ register_callbacks( _In_ PFN_SHUTDOWN_HANDLER shutdown_handler, _In_ PFN_DISCONNECT_HANDLER disconnect_handler, _In_ PFN_ASYNC_COMPLETION_HANDLER async_completion_handler, + _In_ PFN_REQUESTS_DRAINED_HANDLER requestsDrainedHandler, _In_ VOID* pvRequstHandlerContext, _In_ VOID* pvShutdownHandlerContext ) @@ -32,6 +33,7 @@ register_callbacks( shutdown_handler, disconnect_handler, async_completion_handler, + requestsDrainedHandler, pvRequstHandlerContext, pvShutdownHandlerContext ); diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs index 1e20316d272a..08590132cf98 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.cs @@ -556,7 +556,6 @@ private async Task HandleRequest() // Post completion after completing the request to resume the state machine PostCompletion(ConvertRequestCompletionResults(successfulRequest)); - Server.DecrementRequests(); // Dispose the context Dispose(); diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs index 3f35924398bf..de292e56c6cd 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpServer.cs @@ -1,3 +1,5 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Buffers; @@ -23,6 +25,7 @@ internal class IISHttpServer : IServer private static readonly NativeMethods.PFN_SHUTDOWN_HANDLER _shutdownHandler = HandleShutdown; private static readonly NativeMethods.PFN_DISCONNECT_HANDLER _onDisconnect = OnDisconnect; private static readonly NativeMethods.PFN_ASYNC_COMPLETION _onAsyncCompletion = OnAsyncCompletion; + private static readonly NativeMethods.PFN_REQUESTS_DRAINED_HANDLER _requestsDrainedHandler = OnRequestsDrained; private IISContextFactory _iisContextFactory; private readonly MemoryPool _memoryPool = new SlabMemoryPool(); @@ -33,11 +36,9 @@ internal class IISHttpServer : IServer private readonly IISNativeApplication _nativeApplication; private readonly ServerAddressesFeature _serverAddressesFeature; - private volatile int _stopping; - private bool Stopping => _stopping == 1; - private int _outstandingRequests; private readonly TaskCompletionSource _shutdownSignal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); private bool? _websocketAvailable; + private CancellationTokenRegistration _cancellationTokenRegistration; public IFeatureCollection Features { get; } = new FeatureCollection(); @@ -86,7 +87,7 @@ public Task StartAsync(IHttpApplication application, Cancell _httpServerHandle = GCHandle.Alloc(this); _iisContextFactory = new IISContextFactory(_memoryPool, application, _options, this, _logger); - _nativeApplication.RegisterCallbacks(_requestHandler, _shutdownHandler, _onDisconnect, _onAsyncCompletion, (IntPtr)_httpServerHandle, (IntPtr)_httpServerHandle); + _nativeApplication.RegisterCallbacks(_requestHandler, _shutdownHandler, _onDisconnect, _onAsyncCompletion, _requestsDrainedHandler, (IntPtr)_httpServerHandle, (IntPtr)_httpServerHandle); _serverAddressesFeature.Addresses = _options.ServerAddresses; @@ -95,50 +96,18 @@ public Task StartAsync(IHttpApplication application, Cancell public Task StopAsync(CancellationToken cancellationToken) { - void RegisterCancelation() - { - cancellationToken.Register(() => - { - _nativeApplication.StopCallsIntoManaged(); - _shutdownSignal.TrySetResult(null); - }); - } - if (Interlocked.Exchange(ref _stopping, 1) == 1) - { - RegisterCancelation(); - - return _shutdownSignal.Task; - } - - // First call back into native saying "DON'T SEND ME ANY MORE REQUESTS" _nativeApplication.StopIncomingRequests(); - - try + _cancellationTokenRegistration = cancellationToken.Register((shutdownSignal) => { - // Wait for active requests to drain - if (_outstandingRequests > 0) - { - RegisterCancelation(); - } - else - { - // We have drained all requests. Block any callbacks into managed at this point. - _nativeApplication.StopCallsIntoManaged(); - _shutdownSignal.TrySetResult(null); - } - } - catch (Exception ex) - { - _shutdownSignal.TrySetException(ex); - } + ((TaskCompletionSource)shutdownSignal).TrySetResult(null); + }, + _shutdownSignal); return _shutdownSignal.Task; } public void Dispose() { - _stopping = 1; - // Block any more calls into managed from native as we are unloading. _nativeApplication.StopCallsIntoManaged(); _shutdownSignal.TrySetResult(null); @@ -152,21 +121,6 @@ public void Dispose() _nativeApplication.Dispose(); } - private void IncrementRequests() - { - Interlocked.Increment(ref _outstandingRequests); - } - - internal void DecrementRequests() - { - if (Interlocked.Decrement(ref _outstandingRequests) == 0 && Stopping) - { - // All requests have been drained. - _nativeApplication.StopCallsIntoManaged(); - _shutdownSignal.TrySetResult(null); - } - } - private static NativeMethods.REQUEST_NOTIFICATION_STATUS HandleRequest(IntPtr pInProcessHandler, IntPtr pvRequestContext) { IISHttpServer server = null; @@ -174,7 +128,6 @@ private static NativeMethods.REQUEST_NOTIFICATION_STATUS HandleRequest(IntPtr pI { // Unwrap the server so we can create an http context and process the request server = (IISHttpServer)GCHandle.FromIntPtr(pvRequestContext).Target; - server.IncrementRequests(); var context = server._iisContextFactory.CreateHttpContext(pInProcessHandler); @@ -205,7 +158,6 @@ private static bool HandleShutdown(IntPtr pvRequestContext) return true; } - private static void OnDisconnect(IntPtr pvManagedHttpContext) { IISHttpContext context = null; @@ -237,6 +189,23 @@ private static NativeMethods.REQUEST_NOTIFICATION_STATUS OnAsyncCompletion(IntPt } } + private static void OnRequestsDrained(IntPtr serverContext) + { + IISHttpServer server = null; + try + { + server = (IISHttpServer)GCHandle.FromIntPtr(serverContext).Target; + + server._nativeApplication.StopCallsIntoManaged(); + server._shutdownSignal.TrySetResult(null); + server._cancellationTokenRegistration.Dispose(); + } + catch (Exception ex) + { + server?._logger.LogError(0, ex, $"Unexpected exception in {nameof(IISHttpServer)}.{nameof(OnRequestsDrained)}."); + } + } + private class IISContextFactory : IISContextFactory { private readonly IHttpApplication _application; diff --git a/src/Servers/IIS/IIS/src/Core/IISNativeApplication.cs b/src/Servers/IIS/IIS/src/Core/IISNativeApplication.cs index 1be8e888fd25..81436724cc62 100644 --- a/src/Servers/IIS/IIS/src/Core/IISNativeApplication.cs +++ b/src/Servers/IIS/IIS/src/Core/IISNativeApplication.cs @@ -30,6 +30,7 @@ public void RegisterCallbacks( NativeMethods.PFN_SHUTDOWN_HANDLER shutdownHandler, NativeMethods.PFN_DISCONNECT_HANDLER disconnectHandler, NativeMethods.PFN_ASYNC_COMPLETION onAsyncCompletion, + NativeMethods.PFN_REQUESTS_DRAINED_HANDLER requestDrainedHandler, IntPtr requestContext, IntPtr shutdownContext) { @@ -39,6 +40,7 @@ public void RegisterCallbacks( shutdownHandler, disconnectHandler, onAsyncCompletion, + requestDrainedHandler, requestContext, shutdownContext); } diff --git a/src/Servers/IIS/IIS/src/NativeMethods.cs b/src/Servers/IIS/IIS/src/NativeMethods.cs index 4702d29a5736..415604ccc152 100644 --- a/src/Servers/IIS/IIS/src/NativeMethods.cs +++ b/src/Servers/IIS/IIS/src/NativeMethods.cs @@ -44,6 +44,7 @@ public enum REQUEST_NOTIFICATION_STATUS public delegate bool PFN_SHUTDOWN_HANDLER(IntPtr pvRequestContext); public delegate REQUEST_NOTIFICATION_STATUS PFN_ASYNC_COMPLETION(IntPtr pvManagedHttpContext, int hr, int bytes); public delegate REQUEST_NOTIFICATION_STATUS PFN_WEBSOCKET_ASYNC_COMPLETION(IntPtr pInProcessHandler, IntPtr completionInfo, IntPtr pvCompletionContext); + public delegate void PFN_REQUESTS_DRAINED_HANDLER(IntPtr pvRequestContext); [DllImport(AspNetCoreModuleDll)] private static extern int http_post_completion(IntPtr pInProcessHandler, int cbBytes); @@ -60,6 +61,7 @@ private static extern int register_callbacks(IntPtr pInProcessApplication, PFN_SHUTDOWN_HANDLER shutdownCallback, PFN_DISCONNECT_HANDLER disconnectCallback, PFN_ASYNC_COMPLETION asyncCallback, + PFN_REQUESTS_DRAINED_HANDLER requestsDrainedHandler, IntPtr pvRequestContext, IntPtr pvShutdownContext); @@ -160,10 +162,11 @@ public static void HttpRegisterCallbacks(IntPtr pInProcessApplication, PFN_SHUTDOWN_HANDLER shutdownCallback, PFN_DISCONNECT_HANDLER disconnectCallback, PFN_ASYNC_COMPLETION asyncCallback, + PFN_REQUESTS_DRAINED_HANDLER requestsDrainedHandler, IntPtr pvRequestContext, IntPtr pvShutdownContext) { - Validate(register_callbacks(pInProcessApplication, requestCallback, shutdownCallback, disconnectCallback, asyncCallback, pvRequestContext, pvShutdownContext)); + Validate(register_callbacks(pInProcessApplication, requestCallback, shutdownCallback, disconnectCallback, asyncCallback, requestsDrainedHandler, pvRequestContext, pvShutdownContext)); } public static unsafe int HttpWriteResponseBytes(IntPtr pInProcessHandler, HttpApiTypes.HTTP_DATA_CHUNK* pDataChunks, int nChunks, out bool fCompletionExpected) diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/AppOfflineTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/AppOfflineTests.cs index 0da32bfd268d..7d8d661ef043 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/AppOfflineTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/AppOfflineTests.cs @@ -98,7 +98,7 @@ public async Task AppOfflineDroppedWhileSiteFailedToStartInRequestHandler_SiteSt await deploymentResult.AssertRecycledAsync(() => AssertAppOffline(deploymentResult)); } - [ConditionalFact(Skip = "https://github.com/aspnet/AspNetCore/issues/6555")] + [ConditionalFact] [RequiresIIS(IISCapability.ShutdownToken)] public async Task AppOfflineDroppedWhileSiteStarting_SiteShutsDown_InProcess() { diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs index f97001b45f0d..6e2d1bc7521c 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/ClientDisconnectTests.cs @@ -66,7 +66,7 @@ await connection.Send( await _fixture.Client.RetryRequestAsync("/WaitingRequestCount", async message => await message.Content.ReadAsStringAsync() == "0"); } - [ConditionalFact(Skip = "https://github.com/aspnet/AspNetCore/issues/4512")] + [ConditionalFact] public async Task ClientDisconnectCallbackStress() { // Fixture initialization fails if inside of the Task.Run, so send an diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/ShutdownTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/ShutdownTests.cs new file mode 100644 index 000000000000..b30a262b003a --- /dev/null +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/ShutdownTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; +using Microsoft.AspNetCore.Testing.xunit; +using Xunit; + +namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests +{ + [Collection(PublishedSitesCollection.Name)] + public class ShutdownTests : IISFunctionalTestBase + { + private readonly PublishedSitesFixture _fixture; + + public ShutdownTests(PublishedSitesFixture fixture) + { + _fixture = fixture; + } + + [ConditionalFact] + public async Task ShutdownTimeoutIsApplied() + { + var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); + deploymentParameters.TransformArguments((a, _) => $"{a} HangOnStop"); + deploymentParameters.WebConfigActionList.Add( + WebConfigHelpers.AddOrModifyAspNetCoreSection("shutdownTimeLimit", "1")); + + var deploymentResult = await DeployAsync(deploymentParameters); + + Assert.Equal("Hello World", await deploymentResult.HttpClient.GetStringAsync("/HelloWorld")); + + StopServer(); + + EventLogHelpers.VerifyEventLogEvents(deploymentResult, + EventLogHelpers.InProcessStarted(deploymentResult), + EventLogHelpers.InProcessFailedToStop(deploymentResult, "")); + } + + [ConditionalTheory] + [InlineData("/ShutdownStopAsync")] + [InlineData("/ShutdownStopAsyncWithCancelledToken")] + public async Task CallStopAsyncOnRequestThread_DoesNotHangIndefinitely(string path) + { + // Canceled token doesn't affect shutdown, in-proc doesn't handle ungraceful shutdown + // IIS's ShutdownTimeLimit will handle that. + var parameters = _fixture.GetBaseDeploymentParameters(publish: true); + var deploymentResult = await DeployAsync(parameters); + try + { + await deploymentResult.HttpClient.GetAsync(path); + } + catch (HttpRequestException ex) when (ex.InnerException is IOException) + { + // Server might close a connection before request completes + } + + deploymentResult.AssertWorkerProcessStop(); + } + } +} diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs index 4d0279e25294..1cf59697946e 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/Inprocess/StartupTests.cs @@ -340,25 +340,6 @@ public async Task StartupTimeoutIsApplied() ); } - [ConditionalFact] - public async Task ShutdownTimeoutIsApplied() - { - var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite, publish: true); - deploymentParameters.TransformArguments((a, _) => $"{a} HangOnStop"); - deploymentParameters.WebConfigActionList.Add( - WebConfigHelpers.AddOrModifyAspNetCoreSection("shutdownTimeLimit", "1")); - - var deploymentResult = await DeployAsync(deploymentParameters); - - Assert.Equal("Hello World", await deploymentResult.HttpClient.GetStringAsync("/HelloWorld")); - - StopServer(); - - EventLogHelpers.VerifyEventLogEvents(deploymentResult, - EventLogHelpers.InProcessStarted(deploymentResult), - EventLogHelpers.InProcessFailedToStop(deploymentResult, "")); - } - [ConditionalFact] public async Task CheckInvalidHostingModelParameter() { diff --git a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/InProcess/IISExpressShutdownTests.cs similarity index 93% rename from src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs rename to src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/InProcess/IISExpressShutdownTests.cs index b598f8abe77d..bca40e681590 100644 --- a/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/InProcess/ShutdownTests.cs +++ b/src/Servers/IIS/IIS/test/IISExpress.FunctionalTests/InProcess/IISExpressShutdownTests.cs @@ -8,17 +8,18 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities; using Microsoft.AspNetCore.Server.IntegrationTesting; +using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; using Microsoft.AspNetCore.Testing.xunit; using Xunit; namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests { [Collection(PublishedSitesCollection.Name)] - public class ShutdownTests : IISFunctionalTestBase + public class IISExpressShutdownTests : IISFunctionalTestBase { private readonly PublishedSitesFixture _fixture; - public ShutdownTests(PublishedSitesFixture fixture) + public IISExpressShutdownTests(PublishedSitesFixture fixture) { _fixture = fixture; } @@ -41,7 +42,7 @@ public async Task ServerShutsDownWhenMainExits() } - [ConditionalFact(Skip = "https://github.com/aspnet/AspNetCore/issues/6605")] + [ConditionalFact] public async Task ServerShutsDownWhenMainExitsStress() { var parameters = _fixture.GetBaseDeploymentParameters(publish: true); diff --git a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs index 31f69a3b633e..619ac5885554 100644 --- a/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs +++ b/src/Servers/IIS/IIS/test/testassets/InProcessWebSite/Startup.cs @@ -6,10 +6,12 @@ using System.Linq; using System.Net; using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.IISIntegration.FunctionalTests; @@ -656,6 +658,22 @@ private async Task Shutdown(HttpContext ctx) ctx.RequestServices.GetService().StopApplication(); } + private async Task ShutdownStopAsync(HttpContext ctx) + { + await ctx.Response.WriteAsync("Shutting down"); + var server = ctx.RequestServices.GetService(); + await server.StopAsync(default); + } + + private async Task ShutdownStopAsyncWithCancelledToken(HttpContext ctx) + { + await ctx.Response.WriteAsync("Shutting down"); + var server = ctx.RequestServices.GetService(); + var cts = new CancellationTokenSource(); + cts.Cancel(); + await server.StopAsync(cts.Token); + } + private async Task GetServerVariableStress(HttpContext ctx) { // This test simulates the scenario where native Flush call is being