Description
Background and motivation
In order to support scenarios that need hook points before and after IHostedService.StartAsync()
and StopAsync()
, this proposal adds a new interface IHostedLifecycleService
with these hooks points. It derives from IHostedService and is not injected separately from IHostedService
.
This interface is located in the Microsoft.Extensions.Hosting.Abstractions
assembly which will be supported by the default host (in the Microsoft.Extensions.Hosting
assembly). Other hosts will need to implement IHostedService
in order to support this new interface.
See also:
- [API Proposal]: Decouple ValidateOnStart from Hosting #84347
- DI : add support to eager load a singleton service #43149
API Proposal
These located in the Microsoft.Extensions.Hosting.Abstractions assembly.
namespace Microsoft.Extensions.Hosting
{
+ public interface IHostedLifecycleService : IHostedService
+ {
+ // These are awaited before 'IHostedService.StartAsync()' are run.
+ Task StartingAsync(CancellationToken cancellationToken);
+ // These are awaited after 'IHostedService.StartAsync()' are run.
+ Task StartedAsync(CancellationToken cancellationToken);
+ // These are awaited before 'IHostedService.StopAsync()' are run.
+ Task StoppingAsync(CancellationToken cancellationToken);
+ // These are awaited after 'IHostedService.StopAsync()' is run.
+ Task StoppedAsync(CancellationToken cancellationToken);
+ }
}
These located in the Microsoft.Extensions.Hosting assembly:
namespace Microsoft.Extensions.Hosting
{
public class HostOptions
{
// Similar to ShutdownTimeout used in Host.StopAsync(), this creates a CancellationToken with the timeout.
// Applies to Host.StartAsync() encompassing IHostedLifecycleService.StartingAsync(), IHostedService.StartAsync()
// and IHostedLifecycleService.StartedAsync().
// The timeout is off by default, unlike ShutdownTimeout which is 30 seconds.
// This avoids a breaking change since existing implementations of IHostedService.StartAsync() are included in the timeout.
+ public TimeSpan StartupTimeout { get; set; } = Timeout.InfiniteTimeSpan;
}
}
API Usage
IHostBuilder hostBuilder = new HostBuilder();
hostBuilder.ConfigureServices(services =>
{
services.AddHostedService<MyService>();
}
using (IHost host = hostBuilder.Build())
{
await host.StartAsync();
}
public class MyService : IHostedLifecycleService
{
public Task StartingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
public Task StartAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken) => /* add logic here */ Task.CompletedTask;
}
Design notes
- The intent is that the new callbacks are cooperative amongst users and only used for services such as validation and warm-up scenarios that need to occur before\after the existing
IHostedService.StartAsync()
andIHostedService.StopAsync()
.
Ordering
The existing IHostApplicationLifetime which can be used for essentially the same hook points for "Started", "Stopping" and "Stopped" (but not "Starting") although those run serially and do not support an async, Task-based model. The new hook points contain more local semantics and thus are run before the corresponding IHostApplicationLifetime ones which are more "global" and may want to post-process any changes made. The IHostLifetime callbacks always come first\last.
The full lifecycle:
- IHostLifetime.WaitForStartAsync
- IHostedLifecycleService.StartingAsync
- IHostedService.Start
- IHostedLifecycleService.StartedAsync
- IHostApplicationLifetime.ApplicationStarted
- IHostedLifecycleService.StoppingAsync
- IHostApplicationLifetime.ApplicationStopping
- IHostedService.Stop
- IHostedLifecycleService.StoppedAsync
- IHostApplicationLifetime.ApplicationStopped
- IHostLifetime.StopAsync
Exceptions and guarantees
Exception semantics are basically the same: exceptions from callbacks are caught, and when all callbacks are called, the exception(s) are logged and re-thrown.
All callbacks are guaranteed to be called (minus special cases in shutdown). For backwards compat, this means that for the default host at least, once start is called, all handlers for starting, start and stopped will be called even if an exception occurs in one of the earlier phases. The same applies for stop.
The above semantics do not hold for exception thrown from IHostApplicationLifetime callbacks which log but do not re-throw.
Threading
The newer options HostOptions.ServicesStartConcurrently and HostOptions.ServicesStopConcurrently added in V8 support an opt-in for a concurrent mode which runs both the StartAsync and StopAsync callbacks concurrently plus the new callbacks. When the newer concurrent mode is not enabled, the callbacks run serially.
When the concurrent mode is enabled:
- A given hook point, such as a
IHostedLifecycleService.StartingAsync()
, runs serially with otherIHostedLifecycleService.StartingAsync()
implementations in the order of registration until anawait
occurs, if any (or in general, an uncompleted Task is returned). At that point, all such async callbacks are run concurrently. This is to optimize performance since many hook point will returnTask.CompletedTask
if the implementation is a noop. This also allows the author more control over the ordering and semantics of additional async calls. - The StartAsync and\or StopAsync will change to the same optimized serial+concurrent model as explained above for the new callbacks**. Currently StartAsync\StopAsync do not have the "serial" portion and thus run concurrently even for synchronous methods. Changing this is feasible because the main reason for this new V8 feature was to avoid timeouts during a long shutdown, such as when the host needs to drain requests, and that logic should be async already.
Timeouts
- The new timeout will be off by default to avoid a breaking change for existing uses of
StartAsync()
especially in cases whereStartAsync()
is the actual long-running service logic.- The timeout is intended to help diagnose startup issues.
- The existing timeout for stop is 30 seconds in the default host and 5 seconds with ASP.NET although that is expected to change to 30 for consistency.
Alternative Designs
An optional feature for ease-of-use for adding a simple callback via func\delegate (instead of overriding all 6 methods when only 1 is needed) was prototyped but no longer considered for v8 because it would have inconsistent and potentially confusing concurrency, exception and ordering semantics that are different from implementing IHostedLifecycleService
directly.
The implementation would likely use a single instance of IHostedLifecycleService
with a chained delegate for each callback type (starting, started, stopping, stopped and potentially start+stop) with the semantics differing from using an implementation of IHostedLifecycleService
:
- Async but running in series (instead of concurrent for callbacks that aren't all sync)
- The first exception will prevent subsequent delegates from being called (instead of guaranteeing each callback is called for a given method)
- Ordering is relative to other implementations of
IHostedLifecycleService
by the first call to add any delegate and not the individual order (a "builder" pattern would be useful to communicate that).
To get consistent semantics with direct IHostedLifecycleService
implementations requires new public APIs so the host can know about this pattern -- the prototype simply used the abstractions assembly, and the hosting assembly was unaware of this.
E.g.
// For consistency with other IServiceCollection extensions, use the DependencyInjection namspace
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionHostedServiceExtensions
{
// Helper callbacks specifying a delegate for ease-of-use:
+ public static IServiceCollection AddServiceStarting(
+ this IServiceCollection services,
+ System.Func<IServiceProvider, CancellationToken, Task> startingFunc);
+ public static IServiceCollection AddServiceStart(
+ this IServiceCollection services,
+ System.Func<IServiceProvider, CancellationToken, Task> startingFunc);
+ public static IServiceCollection AddServiceStarted(
+ this IServiceCollection services,
+ System.Func<IServiceProvider, CancellationToken, Task> startedFunc);
+ public static IServiceCollection AddServiceStopping(
+ this IServiceCollection services,
+ System.Func<IServiceProvider, CancellationToken, Task> stoppingFunc);
+ public static IServiceCollection AddServiceStop(
+ this IServiceCollection services,
+ System.Func<IServiceProvider, CancellationToken, Task> startingFunc);
+ public static IServiceCollection AddServiceStopped(
+ this IServiceCollection services,
+ System.Func<IServiceProvider, CancellationToken, Task> stoppedFunc);
}
}
Risks
No response