Fix AppOfflineDroppedWhileSiteStarting_SiteShutsDown_InProcess (#8419)

This commit is contained in:
Justin Kotalik 2019-03-13 14:59:41 -07:00 committed by GitHub
parent d329cca17d
commit 1679ba7844
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 189 additions and 43 deletions

View File

@ -155,8 +155,8 @@ APPLICATION_INFO::TryCreateApplication(IHttpContext& pHttpContext, const ShimOpt
} }
LOG_LAST_ERROR_IF(WaitForSingleObject(eventHandle, INFINITE) != WAIT_OBJECT_0); LOG_LAST_ERROR_IF(WaitForSingleObject(eventHandle, INFINITE) != WAIT_OBJECT_0);
} }
} }
RETURN_IF_FAILED(m_handlerResolver.GetApplicationFactory(*pHttpContext.GetApplication(), m_pApplicationFactory, options)); RETURN_IF_FAILED(m_handlerResolver.GetApplicationFactory(*pHttpContext.GetApplication(), m_pApplicationFactory, options));
LOG_INFO(L"Creating handler application"); LOG_INFO(L"Creating handler application");

View File

@ -9,6 +9,7 @@
#include "SRWExclusiveLock.h" #include "SRWExclusiveLock.h"
#include "SRWSharedLock.h" #include "SRWSharedLock.h"
#include "exceptions.h" #include "exceptions.h"
#include "HandleWrapper.h"
class APPLICATION : public IAPPLICATION class APPLICATION : public IAPPLICATION
{ {
@ -24,8 +25,10 @@ public:
{ {
*pRequestHandler = nullptr; *pRequestHandler = nullptr;
SRWSharedLock stopLock(m_stateLock); SRWSharedLock stopLock(m_stopLock);
// If we have acquired the stopLock, we don't need to acquire the data lock
// as m_fStoppedCalled is only set by Stop.
if (m_fStopCalled) if (m_fStopCalled)
{ {
return S_FALSE; return S_FALSE;
@ -49,22 +52,25 @@ public:
m_applicationConfigPath(pHttpApplication.GetAppConfigPath()), m_applicationConfigPath(pHttpApplication.GetAppConfigPath()),
m_applicationId(pHttpApplication.GetApplicationId()) m_applicationId(pHttpApplication.GetApplicationId())
{ {
InitializeSRWLock(&m_stateLock); InitializeSRWLock(&m_stopLock);
InitializeSRWLock(&m_dataLock);
m_applicationVirtualPath = ToVirtualPath(m_applicationConfigPath); m_applicationVirtualPath = ToVirtualPath(m_applicationConfigPath);
} }
VOID VOID
Stop(bool fServerInitiated) override Stop(bool fServerInitiated) override
{ {
SRWExclusiveLock stopLock(m_stateLock); SRWExclusiveLock stopLock(m_stopLock);
if (m_fStopCalled)
{ {
return; SRWExclusiveLock dataLock(m_dataLock);
} if (m_fStopCalled)
{
return;
}
m_fStopCalled = true; m_fStopCalled = true;
}
StopInternal(fServerInitiated); StopInternal(fServerInitiated);
} }
@ -120,7 +126,8 @@ public:
} }
protected: protected:
SRWLOCK m_stateLock {}; SRWLOCK m_stopLock{};
SRWLOCK m_dataLock {};
bool m_fStopCalled; bool m_fStopCalled;
private: private:

View File

@ -20,7 +20,7 @@ IN_PROCESS_APPLICATION::IN_PROCESS_APPLICATION(
IHttpServer& pHttpServer, IHttpServer& pHttpServer,
IHttpApplication& pApplication, IHttpApplication& pApplication,
std::unique_ptr<InProcessOptions> pConfig, std::unique_ptr<InProcessOptions> pConfig,
APPLICATION_PARAMETER *pParameters, APPLICATION_PARAMETER* pParameters,
DWORD nParameters) : DWORD nParameters) :
InProcessApplicationBase(pHttpServer, pApplication), InProcessApplicationBase(pHttpServer, pApplication),
m_Initialized(false), m_Initialized(false),
@ -55,6 +55,7 @@ IN_PROCESS_APPLICATION::StopInternal(bool fServerInitiated)
VOID VOID
IN_PROCESS_APPLICATION::StopClr() IN_PROCESS_APPLICATION::StopClr()
{ {
// This has the state lock around it.
LOG_INFO(L"Stopping CLR"); LOG_INFO(L"Stopping CLR");
if (!m_blockManagedCallbacks) if (!m_blockManagedCallbacks)
@ -69,11 +70,13 @@ IN_PROCESS_APPLICATION::StopClr()
shutdownHandler(m_ShutdownHandlerContext); shutdownHandler(m_ShutdownHandlerContext);
} }
SRWSharedLock dataLock(m_dataLock);
auto requestCount = m_requestCount.load(); auto requestCount = m_requestCount.load();
if (requestCount == 0) if (requestCount == 0)
{ {
LOG_INFO(L"Drained all requests, notifying managed."); CallRequestsDrained();
m_RequestsDrainedHandler(m_ShutdownHandlerContext);
} }
} }
@ -141,12 +144,6 @@ IN_PROCESS_APPLICATION::LoadManagedApplication()
FALSE, // not set FALSE, // not set
nullptr)); // name 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"); LOG_INFO(L"Waiting for initialization");
m_workerThread = std::thread([](std::unique_ptr<IN_PROCESS_APPLICATION, IAPPLICATION_DELETER> application) m_workerThread = std::thread([](std::unique_ptr<IN_PROCESS_APPLICATION, IAPPLICATION_DELETER> application)
@ -532,8 +529,13 @@ IN_PROCESS_APPLICATION::CreateHandler(
{ {
try try
{ {
SRWSharedLock dataLock(m_dataLock);
DBG_ASSERT(!m_fStopCalled); DBG_ASSERT(!m_fStopCalled);
m_requestCount++; m_requestCount++;
LOG_TRACEF(L"Adding request. Total Request Count %d", m_requestCount.load());
*pRequestHandler = new IN_PROCESS_HANDLER(::ReferenceApplication(this), pHttpContext, m_RequestHandler, m_RequestHandlerContext, m_DisconnectHandler, m_AsyncCompletionHandler); *pRequestHandler = new IN_PROCESS_HANDLER(::ReferenceApplication(this), pHttpContext, m_RequestHandler, m_RequestHandlerContext, m_DisconnectHandler, m_AsyncCompletionHandler);
} }
CATCH_RETURN(); CATCH_RETURN();
@ -544,11 +546,24 @@ IN_PROCESS_APPLICATION::CreateHandler(
void void
IN_PROCESS_APPLICATION::HandleRequestCompletion() IN_PROCESS_APPLICATION::HandleRequestCompletion()
{ {
SRWSharedLock lock(m_stateLock); SRWSharedLock dataLock(m_dataLock);
auto requestCount = m_requestCount--;
if (m_fStopCalled && requestCount == 0) auto requestCount = --m_requestCount;
LOG_TRACEF(L"Removing Request %d", requestCount);
if (m_fStopCalled && requestCount == 0 && !m_blockManagedCallbacks)
{
CallRequestsDrained();
}
}
void IN_PROCESS_APPLICATION::CallRequestsDrained()
{
if (m_RequestsDrainedHandler != nullptr)
{ {
LOG_INFO(L"Drained all requests, notifying managed."); LOG_INFO(L"Drained all requests, notifying managed.");
m_RequestsDrainedHandler(m_ShutdownHandlerContext); m_RequestsDrainedHandler(m_ShutdownHandlerContext);
m_RequestsDrainedHandler = nullptr;
} }
} }

View File

@ -150,8 +150,6 @@ private:
// The event that gets triggered when worker thread should exit // The event that gets triggered when worker thread should exit
HandleWrapper<NullHandleTraits> m_pShutdownEvent; HandleWrapper<NullHandleTraits> m_pShutdownEvent;
HandleWrapper<NullHandleTraits> m_pRequestDrainEvent;
// The request handler callback from managed code // The request handler callback from managed code
PFN_REQUEST_HANDLER m_RequestHandler; PFN_REQUEST_HANDLER m_RequestHandler;
VOID* m_RequestHandlerContext; VOID* m_RequestHandlerContext;
@ -169,6 +167,7 @@ private:
std::atomic_bool m_blockManagedCallbacks; std::atomic_bool m_blockManagedCallbacks;
bool m_Initialized; bool m_Initialized;
bool m_waitForShutdown; bool m_waitForShutdown;
std::atomic<int> m_requestCount; std::atomic<int> m_requestCount;
std::unique_ptr<InProcessOptions> m_pConfig; std::unique_ptr<InProcessOptions> m_pConfig;
@ -188,6 +187,9 @@ private:
void void
StopClr(); StopClr();
void
CallRequestsDrained();
static static
void void
ClrThreadEntryPoint(const std::shared_ptr<ExecuteClrContext> &context); ClrThreadEntryPoint(const std::shared_ptr<ExecuteClrContext> &context);

View File

@ -18,7 +18,7 @@ OUT_OF_PROCESS_APPLICATION::OUT_OF_PROCESS_APPLICATION(
OUT_OF_PROCESS_APPLICATION::~OUT_OF_PROCESS_APPLICATION() OUT_OF_PROCESS_APPLICATION::~OUT_OF_PROCESS_APPLICATION()
{ {
SRWExclusiveLock lock(m_stateLock); SRWExclusiveLock lock(m_stopLock);
if (m_pProcessManager != NULL) if (m_pProcessManager != NULL)
{ {
m_pProcessManager->Shutdown(); m_pProcessManager->Shutdown();

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Xunit; using Xunit;
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS; using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
{ {
@ -98,8 +99,8 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
await deploymentResult.AssertRecycledAsync(() => AssertAppOffline(deploymentResult)); await deploymentResult.AssertRecycledAsync(() => AssertAppOffline(deploymentResult));
} }
[ConditionalFact(Skip = "https://github.com/aspnet/AspNetCore/issues/6555")] [ConditionalFact]
[RequiresIIS(IISCapability.ShutdownToken)] [RequiresNewHandler]
public async Task AppOfflineDroppedWhileSiteStarting_SiteShutsDown_InProcess() public async Task AppOfflineDroppedWhileSiteStarting_SiteShutsDown_InProcess()
{ {
// This test often hits a race between debug logging and stdout redirection closing the handle // This test often hits a race between debug logging and stdout redirection closing the handle
@ -131,19 +132,100 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
// if AssertAppOffline succeeded ANCM have picked up app_offline before starting the app // if AssertAppOffline succeeded ANCM have picked up app_offline before starting the app
// try again // try again
RemoveAppOffline(deploymentResult.ContentRoot); RemoveAppOffline(deploymentResult.ContentRoot);
if (deploymentResult.DeploymentParameters.ServerType == ServerType.IIS)
{
deploymentResult.AssertWorkerProcessStop();
return;
}
} }
catch catch
{ {
// For IISExpress, we need to catch the exception because IISExpress will not restart a process if it crashed.
// RemoveAppOffline will fail due to a bad request exception as the server is down.
Assert.Contains(TestSink.Writes, context => context.Message.Contains("Drained all requests, notifying managed."));
deploymentResult.AssertWorkerProcessStop(); deploymentResult.AssertWorkerProcessStop();
return; return;
} }
} }
Assert.True(false); Assert.True(false);
} }
} }
[ConditionalFact]
public async Task GracefulShutdownWorksWithMultipleRequestsInFlight_InProcess()
{
// The goal of this test is to have multiple requests currently in progress
// and for app offline to be dropped. We expect that all requests are eventually drained
// and graceful shutdown occurs.
var deploymentParameters = _fixture.GetBaseDeploymentParameters(_fixture.InProcessTestSite);
deploymentParameters.TransformArguments((a, _) => $"{a} IncreaseShutdownLimit");
var deploymentResult = await DeployAsync(deploymentParameters);
var result = await deploymentResult.HttpClient.GetAsync("/HelloWorld");
// Send two requests that will hang until data is sent from the client.
var connectionList = new List<TestConnection>();
for (var i = 0; i < 2; i++)
{
var connection = new TestConnection(deploymentResult.HttpClient.BaseAddress.Port);
await connection.Send(
"POST /ReadAndCountRequestBody HTTP/1.1",
"Content-Length: 1",
"Host: localhost",
"Connection: close",
"",
"");
await connection.Receive(
"HTTP/1.1 200 OK", "");
await connection.ReceiveHeaders();
await connection.Receive("1", $"{i + 1}");
connectionList.Add(connection);
}
// Send a request that will end once app lifetime is triggered (ApplicationStopping cts).
var statusConnection = new TestConnection(deploymentResult.HttpClient.BaseAddress.Port);
await statusConnection.Send(
"GET /WaitForAppToStartShuttingDown HTTP/1.1",
"Host: localhost",
"Connection: close",
"",
"");
await statusConnection.Receive("HTTP/1.1 200 OK",
"");
await statusConnection.ReceiveHeaders();
// Receiving some data means we are currently waiting for IHostApplicationLifetime.
await statusConnection.Receive("5",
"test1",
"");
AddAppOffline(deploymentResult.ContentRoot);
// Receive the rest of all open connections.
await statusConnection.Receive("5", "test2", "");
for (var i = 0; i < 2; i++)
{
await connectionList[i].Send("a", "");
await connectionList[i].Receive("", "4", "done");
connectionList[i].Dispose();
}
deploymentResult.AssertWorkerProcessStop();
// Shutdown should be graceful here!
EventLogHelpers.VerifyEventLogEvent(deploymentResult,
EventLogHelpers.InProcessShutdown());
}
[ConditionalFact] [ConditionalFact]
public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_InProcess() public async Task AppOfflineDroppedWhileSiteRunning_SiteShutsDown_InProcess()
{ {
@ -282,6 +364,5 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
File.Delete(file); File.Delete(file);
} }
} }
} }
} }

View File

@ -172,6 +172,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
} }
} }
public static string InProcessShutdown()
{
return "Application 'MACHINE/WEBROOT/APPHOST/.*?' has shutdown.";
}
public static string InProcessFailedToStop(IISDeploymentResult deploymentResult, string reason) public static string InProcessFailedToStop(IISDeploymentResult deploymentResult, string reason)
{ {
return "Failed to gracefully shutdown application 'MACHINE/WEBROOT/APPHOST/.*?'."; return "Failed to gracefully shutdown application 'MACHINE/WEBROOT/APPHOST/.*?'.";

View File

@ -12,11 +12,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
Websockets = 1, Websockets = 1,
WindowsAuthentication = 2, WindowsAuthentication = 2,
PoolEnvironmentVariables = 4, PoolEnvironmentVariables = 4,
ShutdownToken = 8, DynamicCompression = 8,
DynamicCompression = 16, ApplicationInitialization = 16,
ApplicationInitialization = 32, TracingModule = 32,
TracingModule = 64, FailedRequestTracingModule = 64,
FailedRequestTracingModule = 128, BasicAuthentication = 128
BasicAuthentication = 256
} }
} }

View File

@ -124,12 +124,6 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
} }
} }
if (capabilities.HasFlag(IISCapability.ShutdownToken))
{
IsMet = false;
SkipReason += "https://github.com/aspnet/IISIntegration/issues/1074";
}
foreach (var module in Modules) foreach (var module in Modules)
{ {
if (capabilities.HasFlag(module.Capability)) if (capabilities.HasFlag(module.Capability))

View File

@ -63,6 +63,18 @@ namespace TestSite
Thread.Sleep(Timeout.Infinite); Thread.Sleep(Timeout.Infinite);
} }
break;
case "IncreaseShutdownLimit":
{
var host = new WebHostBuilder()
.UseIIS()
.UseShutdownTimeout(TimeSpan.FromSeconds(120))
.UseStartup<Startup>()
.Build();
host.Run();
}
break; break;
case "CheckConsoleFunctions": case "CheckConsoleFunctions":
// Call a bunch of console functions and make sure none return invalid handle. // Call a bunch of console functions and make sure none return invalid handle.

View File

@ -284,6 +284,28 @@ namespace TestSite
{ {
result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1); result = await ctx.Request.Body.ReadAsync(readBuffer, 0, 1);
} }
}
private int _requestsInFlight = 0;
private async Task ReadAndCountRequestBody(HttpContext ctx)
{
Interlocked.Increment(ref _requestsInFlight);
await ctx.Response.WriteAsync(_requestsInFlight.ToString());
var readBuffer = new byte[1];
await ctx.Request.Body.ReadAsync(readBuffer, 0, 1);
await ctx.Response.WriteAsync("done");
Interlocked.Decrement(ref _requestsInFlight);
}
private async Task WaitForAppToStartShuttingDown(HttpContext ctx)
{
await ctx.Response.WriteAsync("test1");
var lifetime = ctx.RequestServices.GetService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.WaitHandle.WaitOne();
await ctx.Response.WriteAsync("test2");
} }
private async Task ReadFullBody(HttpContext ctx) private async Task ReadFullBody(HttpContext ctx)

View File

@ -24,12 +24,13 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS
private const string DetailedErrorsEnvironmentVariable = "ASPNETCORE_DETAILEDERRORS"; private const string DetailedErrorsEnvironmentVariable = "ASPNETCORE_DETAILEDERRORS";
private static readonly TimeSpan _timeout = TimeSpan.FromSeconds(60); private static readonly TimeSpan _timeout = TimeSpan.FromSeconds(60);
private static readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(200); private static readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(100);
private CancellationTokenSource _hostShutdownToken = new CancellationTokenSource(); private CancellationTokenSource _hostShutdownToken = new CancellationTokenSource();
private string _configPath; private string _configPath;
private string _debugLogFile; private string _debugLogFile;
private bool _disposed;
public Process HostProcess { get; set; } public Process HostProcess { get; set; }
@ -45,6 +46,12 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS
public override void Dispose() public override void Dispose()
{ {
if (_disposed)
{
return;
}
_disposed = true;
Dispose(gracefulShutdown: false); Dispose(gracefulShutdown: false);
} }
@ -420,6 +427,7 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS
List<Exception> exceptions = null; List<Exception> exceptions = null;
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
int retryCount = 0; int retryCount = 0;
var delay = _retryDelay;
while (sw.Elapsed < _timeout) while (sw.Elapsed < _timeout)
{ {
@ -443,7 +451,8 @@ namespace Microsoft.AspNetCore.Server.IntegrationTesting.IIS
} }
retryCount++; retryCount++;
Thread.Sleep(_retryDelay); Thread.Sleep(delay);
delay *= 1.5;
} }
throw new AggregateException($"Operation did not succeed after {retryCount} retries", exceptions.ToArray()); throw new AggregateException($"Operation did not succeed after {retryCount} retries", exceptions.ToArray());