Introducing IApplicationLifetimeEvents (#875)
- Introduce a new DI friendly API for handling lifetime events. IApplicationLifetime isn't isn't replaceable so we introduce a new DI friendly API that can be implemented to handle lifetime events of an ASP.NET application. It should also make it possible to write up extension on IWebHostBuilder to wire up to external systems that need to register the state of the application (like systemd). - Run all handlers even if one throws - Let both sets of event handlers run before throwing (IApplicationLifetimeEvents and IApplicationLifetime cancellation token callbacks).
This commit is contained in:
parent
fb5c0f8e5c
commit
42594afd42
|
|
@ -0,0 +1,31 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows consumers to perform cleanup during a graceful shutdown.
|
||||
/// </summary>
|
||||
public interface IApplicationLifetimeEvents
|
||||
{
|
||||
/// <summary>
|
||||
/// Triggered when the application host has fully started and is about to wait
|
||||
/// for a graceful shutdown.
|
||||
/// </summary>
|
||||
void OnApplicationStarted();
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the application host is performing a graceful shutdown.
|
||||
/// Requests may still be in flight. Shutdown will block until this event completes.
|
||||
/// </summary>
|
||||
void OnApplicationStopping();
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the application host is performing a graceful shutdown.
|
||||
/// All requests should be complete at this point. Shutdown will block
|
||||
/// until this event completes.
|
||||
/// </summary>
|
||||
void OnApplicationStopped();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Hosting.Internal
|
||||
{
|
||||
|
|
@ -14,6 +17,14 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
private readonly CancellationTokenSource _startedSource = new CancellationTokenSource();
|
||||
private readonly CancellationTokenSource _stoppingSource = new CancellationTokenSource();
|
||||
private readonly CancellationTokenSource _stoppedSource = new CancellationTokenSource();
|
||||
private readonly IEnumerable<IApplicationLifetimeEvents> _handlers = Enumerable.Empty<IApplicationLifetimeEvents>();
|
||||
private readonly ILogger<ApplicationLifetime> _logger;
|
||||
|
||||
public ApplicationLifetime(ILogger<ApplicationLifetime> logger, IEnumerable<IApplicationLifetimeEvents> handlers)
|
||||
{
|
||||
_logger = logger;
|
||||
_handlers = handlers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the application host has fully started and is about to wait
|
||||
|
|
@ -46,11 +57,13 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
{
|
||||
try
|
||||
{
|
||||
_stoppingSource.Cancel(throwOnFirstException: false);
|
||||
ExecuteHandlers(_stoppingSource, handler => handler.OnApplicationStopping());
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: LOG
|
||||
_logger.ApplicationError(LoggerEventIds.ApplicationStoppingException,
|
||||
"An error occurred stopping the application",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,11 +75,13 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
{
|
||||
try
|
||||
{
|
||||
_startedSource.Cancel(throwOnFirstException: false);
|
||||
ExecuteHandlers(_startedSource, handler => handler.OnApplicationStarted());
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: LOG
|
||||
_logger.ApplicationError(LoggerEventIds.ApplicationStartupException,
|
||||
"An error occurred starting the application",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -77,11 +92,63 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
{
|
||||
try
|
||||
{
|
||||
_stoppedSource.Cancel(throwOnFirstException: false);
|
||||
ExecuteHandlers(_stoppedSource, handler => handler.OnApplicationStopped());
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: LOG
|
||||
_logger.ApplicationError(LoggerEventIds.ApplicationStoppedException,
|
||||
"An error occurred stopping the application",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteHandlers(CancellationTokenSource cancel, Action<IApplicationLifetimeEvents> callback)
|
||||
{
|
||||
// Noop if this is already cancelled
|
||||
if (cancel.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<Exception> exceptions = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Run the cancellation token callbacks
|
||||
cancel.Cancel(throwOnFirstException: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (exceptions == null)
|
||||
{
|
||||
exceptions = new List<Exception>();
|
||||
}
|
||||
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
|
||||
// Run the handlers
|
||||
foreach (var handler in _handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
callback(handler);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (exceptions == null)
|
||||
{
|
||||
exceptions = new List<Exception>();
|
||||
}
|
||||
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Throw an aggregate exception if there were any exceptions
|
||||
if (exceptions != null)
|
||||
{
|
||||
throw new AggregateException(exceptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,14 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
|
||||
public static void ApplicationError(this ILogger logger, Exception exception)
|
||||
{
|
||||
var message = "Application startup exception";
|
||||
logger.ApplicationError(
|
||||
eventId: LoggerEventIds.ApplicationStartupException,
|
||||
message: "Application startup exception",
|
||||
exception: exception);
|
||||
}
|
||||
|
||||
public static void ApplicationError(this ILogger logger, EventId eventId, string message, Exception exception)
|
||||
{
|
||||
var reflectionTypeLoadException = exception as ReflectionTypeLoadException;
|
||||
if (reflectionTypeLoadException != null)
|
||||
{
|
||||
|
|
@ -64,7 +70,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
}
|
||||
|
||||
logger.LogCritical(
|
||||
eventId: LoggerEventIds.ApplicationStartupException,
|
||||
eventId: eventId,
|
||||
message: message,
|
||||
exception: exception);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,5 +11,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
public const int Started = 4;
|
||||
public const int Shutdown = 5;
|
||||
public const int ApplicationStartupException = 6;
|
||||
public const int ApplicationStoppingException = 7;
|
||||
public const int ApplicationStoppedException = 8;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting.Builder;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
|
|
@ -28,9 +27,9 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
|
||||
private readonly IServiceCollection _applicationServiceCollection;
|
||||
private IStartup _startup;
|
||||
private ApplicationLifetime _applicationLifetime;
|
||||
|
||||
private readonly IServiceProvider _hostingServiceProvider;
|
||||
private readonly ApplicationLifetime _applicationLifetime;
|
||||
private readonly WebHostOptions _options;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
|
|
@ -68,8 +67,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
_options = options;
|
||||
_applicationServiceCollection = appServices;
|
||||
_hostingServiceProvider = hostingServiceProvider;
|
||||
_applicationLifetime = new ApplicationLifetime();
|
||||
_applicationServiceCollection.AddSingleton<IApplicationLifetime>(_applicationLifetime);
|
||||
_applicationServiceCollection.AddSingleton<IApplicationLifetime, ApplicationLifetime>();
|
||||
}
|
||||
|
||||
public IServiceProvider Services
|
||||
|
|
@ -101,6 +99,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
{
|
||||
Initialize();
|
||||
|
||||
_applicationLifetime = _applicationServices.GetRequiredService<IApplicationLifetime>() as ApplicationLifetime;
|
||||
_logger = _applicationServices.GetRequiredService<ILogger<WebHost>>();
|
||||
var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticSource>();
|
||||
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
|
||||
|
|
@ -109,7 +108,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
|
||||
Server.Start(new HostingApplication(_application, _logger, diagnosticSource, httpContextFactory));
|
||||
|
||||
_applicationLifetime.NotifyStarted();
|
||||
_applicationLifetime?.NotifyStarted();
|
||||
_logger.Started();
|
||||
}
|
||||
|
||||
|
|
@ -244,10 +243,10 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
|||
public void Dispose()
|
||||
{
|
||||
_logger?.Shutdown();
|
||||
_applicationLifetime.StopApplication();
|
||||
_applicationLifetime?.StopApplication();
|
||||
(_hostingServiceProvider as IDisposable)?.Dispose();
|
||||
(_applicationServices as IDisposable)?.Dispose();
|
||||
_applicationLifetime.NotifyStopped();
|
||||
_applicationLifetime?.NotifyStopped();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,6 +270,135 @@ namespace Microsoft.AspNetCore.Hosting
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebHostNotifiesAllIApplicationLifetimeCallbacksEvenIfTheyThrow()
|
||||
{
|
||||
var host = CreateBuilder()
|
||||
.UseServer(this)
|
||||
.Build();
|
||||
var applicationLifetime = host.Services.GetService<IApplicationLifetime>();
|
||||
|
||||
var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted);
|
||||
var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
|
||||
var stopped = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopped);
|
||||
|
||||
using (host)
|
||||
{
|
||||
host.Start();
|
||||
Assert.True(applicationLifetime.ApplicationStarted.IsCancellationRequested);
|
||||
Assert.True(started.All(s => s));
|
||||
host.Dispose();
|
||||
Assert.True(stopping.All(s => s));
|
||||
Assert.True(stopped.All(s => s));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebHostNotifiesAllIApplicationLifetimeEventsCallbacksEvenIfTheyThrow()
|
||||
{
|
||||
bool[] events1 = null;
|
||||
bool[] events2 = null;
|
||||
|
||||
var host = CreateBuilder()
|
||||
.UseServer(this)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
events1 = RegisterCallbacksThatThrow(services);
|
||||
events2 = RegisterCallbacksThatThrow(services);
|
||||
})
|
||||
.Build();
|
||||
|
||||
using (host)
|
||||
{
|
||||
host.Start();
|
||||
Assert.True(events1[0]);
|
||||
Assert.True(events2[0]);
|
||||
host.Dispose();
|
||||
Assert.True(events1[1]);
|
||||
Assert.True(events2[1]);
|
||||
Assert.True(events1[2]);
|
||||
Assert.True(events2[2]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebHostStopCallbacksDontMultipleTimes()
|
||||
{
|
||||
var stoppingCalls = 0;
|
||||
var stoppedCalls = 0;
|
||||
|
||||
var host = CreateBuilder()
|
||||
.UseServer(this)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
Action started = () =>
|
||||
{
|
||||
};
|
||||
|
||||
Action stopping = () =>
|
||||
{
|
||||
stoppingCalls++;
|
||||
};
|
||||
|
||||
Action stopped = () =>
|
||||
{
|
||||
stoppedCalls++;
|
||||
};
|
||||
|
||||
services.AddSingleton<IApplicationLifetimeEvents>(new DelegateLifetimeEvents(started, stopping, stopped));
|
||||
})
|
||||
.Build();
|
||||
var lifetime = host.Services.GetRequiredService<IApplicationLifetime>();
|
||||
lifetime.StopApplication();
|
||||
lifetime.StopApplication();
|
||||
lifetime.StopApplication();
|
||||
|
||||
using (host)
|
||||
{
|
||||
host.Start();
|
||||
host.Dispose();
|
||||
|
||||
Assert.Equal(1, stoppingCalls);
|
||||
Assert.Equal(1, stoppedCalls);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebHostNotifiesAllIApplicationLifetimeEventsCallbacksAndIApplicationLifetimeCallbacksEvenIfTheyThrow()
|
||||
{
|
||||
bool[] events1 = null;
|
||||
bool[] events2 = null;
|
||||
|
||||
var host = CreateBuilder()
|
||||
.UseServer(this)
|
||||
.ConfigureServices(services =>
|
||||
{
|
||||
events1 = RegisterCallbacksThatThrow(services);
|
||||
events2 = RegisterCallbacksThatThrow(services);
|
||||
})
|
||||
.Build();
|
||||
var applicationLifetime = host.Services.GetService<IApplicationLifetime>();
|
||||
|
||||
var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted);
|
||||
var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
|
||||
var stopped = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopped);
|
||||
|
||||
using (host)
|
||||
{
|
||||
host.Start();
|
||||
Assert.True(events1[0]);
|
||||
Assert.True(events2[0]);
|
||||
Assert.True(started.All(s => s));
|
||||
host.Dispose();
|
||||
Assert.True(events1[1]);
|
||||
Assert.True(events2[1]);
|
||||
Assert.True(stopping.All(s => s));
|
||||
Assert.True(events1[2]);
|
||||
Assert.True(events2[2]);
|
||||
Assert.True(stopped.All(s => s));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WebHostInjectsHostingEnvironment()
|
||||
{
|
||||
|
|
@ -523,6 +652,48 @@ namespace Microsoft.AspNetCore.Hosting
|
|||
return new WebHostBuilder().UseConfiguration(config ?? new ConfigurationBuilder().Build()).UseStartup("Microsoft.AspNetCore.Hosting.Tests");
|
||||
}
|
||||
|
||||
private static bool[] RegisterCallbacksThatThrow(IServiceCollection services)
|
||||
{
|
||||
bool[] events = new bool[3];
|
||||
|
||||
Action started = () =>
|
||||
{
|
||||
events[0] = true;
|
||||
throw new InvalidOperationException();
|
||||
};
|
||||
|
||||
Action stopping = () =>
|
||||
{
|
||||
events[1] = true;
|
||||
throw new InvalidOperationException();
|
||||
};
|
||||
|
||||
Action stopped = () =>
|
||||
{
|
||||
events[2] = true;
|
||||
throw new InvalidOperationException();
|
||||
};
|
||||
|
||||
services.AddSingleton<IApplicationLifetimeEvents>(new DelegateLifetimeEvents(started, stopping, stopped));
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private static bool[] RegisterCallbacksThatThrow(CancellationToken token)
|
||||
{
|
||||
var signals = new bool[3];
|
||||
for (int i = 0; i < signals.Length; i++)
|
||||
{
|
||||
token.Register(state =>
|
||||
{
|
||||
signals[(int)state] = true;
|
||||
throw new InvalidOperationException();
|
||||
}, i);
|
||||
}
|
||||
|
||||
return signals;
|
||||
}
|
||||
|
||||
public void Start<TContext>(IHttpApplication<TContext> application)
|
||||
{
|
||||
var startInstance = new StartInstance();
|
||||
|
|
@ -551,6 +722,26 @@ namespace Microsoft.AspNetCore.Hosting
|
|||
}
|
||||
}
|
||||
|
||||
private class DelegateLifetimeEvents : IApplicationLifetimeEvents
|
||||
{
|
||||
private readonly Action _started;
|
||||
private readonly Action _stopping;
|
||||
private readonly Action _stopped;
|
||||
|
||||
public DelegateLifetimeEvents(Action started, Action stopping, Action stopped)
|
||||
{
|
||||
_started = started;
|
||||
_stopping = stopping;
|
||||
_stopped = stopped;
|
||||
}
|
||||
|
||||
public void OnApplicationStarted() => _started();
|
||||
|
||||
public void OnApplicationStopped() => _stopped();
|
||||
|
||||
public void OnApplicationStopping() => _stopping();
|
||||
}
|
||||
|
||||
private class StartInstance : IDisposable
|
||||
{
|
||||
public int DisposeCalls { get; set; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue