diff --git a/src/Microsoft.AspNetCore.Hosting/Internal/ApplicationLifetime.cs b/src/Microsoft.AspNetCore.Hosting/Internal/ApplicationLifetime.cs index d2cd26f6d9..dbbf97d181 100644 --- a/src/Microsoft.AspNetCore.Hosting/Internal/ApplicationLifetime.cs +++ b/src/Microsoft.AspNetCore.Hosting/Internal/ApplicationLifetime.cs @@ -39,13 +39,19 @@ namespace Microsoft.AspNetCore.Hosting.Internal /// public void StopApplication() { - try + // Lock on CTS to synchronize multiple calls to StopApplication. This guarantees that the first call + // to StopApplication and its callbacks run to completion before subsequent calls to StopApplication, + // which will no-op since the first call already requested cancellation, get a chance to execute. + lock (_stoppingSource) { - _stoppingSource.Cancel(throwOnFirstException: false); - } - catch (Exception) - { - // TODO: LOG + try + { + _stoppingSource.Cancel(throwOnFirstException: false); + } + catch (Exception) + { + // TODO: LOG + } } } diff --git a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs index b63e3cd834..33b647eeeb 100644 --- a/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs +++ b/test/Microsoft.AspNetCore.Hosting.Tests/WebHostTests.cs @@ -166,6 +166,67 @@ namespace Microsoft.AspNetCore.Hosting Assert.Equal(1, _startInstances[0].DisposeCalls); } + [Fact] + public void WebHostApplicationLifetimeEventsOrderedCorrectlyDuringShutdown() + { + var host = CreateBuilder() + .UseServer(this) + .UseStartup("Microsoft.AspNetCore.Hosting.Tests") + .Build(); + + var lifetime = host.Services.GetRequiredService(); + var applicationStartedEvent = new ManualResetEventSlim(false); + var applicationStoppingEvent = new ManualResetEventSlim(false); + var applicationStoppedEvent = new ManualResetEventSlim(false); + var applicationStartedCompletedBeforeApplicationStopping = false; + var applicationStoppingCompletedBeforeApplicationStopped = false; + var applicationStoppedCompletedBeforeRunCompleted = false; + + lifetime.ApplicationStarted.Register(() => + { + applicationStartedEvent.Set(); + }); + + lifetime.ApplicationStopping.Register(() => + { + // Check whether the applicationStartedEvent has been set + applicationStartedCompletedBeforeApplicationStopping = applicationStartedEvent.IsSet; + + // Simulate work. + Thread.Sleep(1000); + + applicationStoppingEvent.Set(); + }); + + lifetime.ApplicationStopped.Register(() => + { + // Check whether the applicationStoppingEvent has been set + applicationStoppingCompletedBeforeApplicationStopped = applicationStoppingEvent.IsSet; + applicationStoppedEvent.Set(); + }); + + var runHostAndVerifyApplicationStopped = Task.Run(() => + { + host.Run(); + // Check whether the applicationStoppingEvent has been set + applicationStoppedCompletedBeforeRunCompleted = applicationStoppedEvent.IsSet; + }); + + // Wait until application has started to shut down the host + Assert.True(applicationStartedEvent.Wait(5000)); + + // Trigger host shutdown on a separate thread + Task.Run(() => lifetime.StopApplication()); + + // Wait for all events and host.Run() to complete + Assert.True(runHostAndVerifyApplicationStopped.Wait(5000)); + + // Verify Ordering + Assert.True(applicationStartedCompletedBeforeApplicationStopping); + Assert.True(applicationStoppingCompletedBeforeApplicationStopped); + Assert.True(applicationStoppedCompletedBeforeRunCompleted); + } + [Fact] public void WebHostDisposesServiceProvider() {