From abcd91ce89a3d47cd140d4973afd8c2badfa4dd7 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Fri, 30 Jun 2023 16:04:00 -0500 Subject: [PATCH] Add IHostedLifecycleService (#87335) --- ...crosoft.Extensions.Hosting.Abstractions.cs | 7 + .../src/IHostedLifecycleService.cs | 44 +++ .../ref/Microsoft.Extensions.Hosting.cs | 1 + .../src/HostOptions.cs | 25 +- .../src/Internal/Host.cs | 308 ++++++++++----- .../tests/UnitTests/DisposeTests.cs | 99 +++++ .../tests/UnitTests/LifecycleTests.Start.cs | 338 +++++++++++++++++ .../tests/UnitTests/LifecycleTests.Stop.cs | 350 ++++++++++++++++++ .../UnitTests/LifecycleTests.Timeouts.cs | 169 +++++++++ .../tests/UnitTests/LifecycleTests.cs | 285 ++++++++++++++ 10 files changed, 1530 insertions(+), 96 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostedLifecycleService.cs create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DisposeTests.cs create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Timeouts.cs create mode 100644 src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs index 88f37fc954622..cc663649830ad 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/ref/Microsoft.Extensions.Hosting.Abstractions.cs @@ -124,6 +124,13 @@ public partial interface IHostBuilder Microsoft.Extensions.Hosting.IHostBuilder UseServiceProviderFactory(Microsoft.Extensions.DependencyInjection.IServiceProviderFactory factory) where TContainerBuilder : notnull; Microsoft.Extensions.Hosting.IHostBuilder UseServiceProviderFactory(System.Func> factory) where TContainerBuilder : notnull; } + public partial interface IHostedLifecycleService : Microsoft.Extensions.Hosting.IHostedService + { + System.Threading.Tasks.Task StartedAsync(System.Threading.CancellationToken cancellationToken); + System.Threading.Tasks.Task StartingAsync(System.Threading.CancellationToken cancellationToken); + System.Threading.Tasks.Task StoppedAsync(System.Threading.CancellationToken cancellationToken); + System.Threading.Tasks.Task StoppingAsync(System.Threading.CancellationToken cancellationToken); + } public partial interface IHostedService { System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken); diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostedLifecycleService.cs b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostedLifecycleService.cs new file mode 100644 index 0000000000000..114308606e80f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/IHostedLifecycleService.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using System.Threading; + +namespace Microsoft.Extensions.Hosting +{ + /// + /// Defines methods that are run before or after + /// and + /// . + /// + public interface IHostedLifecycleService : IHostedService + { + /// + /// Triggered before . + /// + /// Indicates that the start process has been aborted. + /// A that represents the asynchronous operation. + Task StartingAsync(CancellationToken cancellationToken); + + /// + /// Triggered after . + /// + /// Indicates that the start process has been aborted. + /// A that represents the asynchronous operation. + Task StartedAsync(CancellationToken cancellationToken); + + /// + /// Triggered before . + /// + /// Indicates that the start process has been aborted. + /// A that represents the asynchronous operation. + Task StoppingAsync(CancellationToken cancellationToken); + + /// + /// Triggered after . + /// + /// Indicates that the stop process has been aborted. + /// A that represents the asynchronous operation. + Task StoppedAsync(CancellationToken cancellationToken); + } +} diff --git a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs index 4e7804b87d1ca..0085fcae802df 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs @@ -110,6 +110,7 @@ public HostOptions() { } public bool ServicesStartConcurrently { get { throw null; } set { } } public bool ServicesStopConcurrently { get { throw null; } set { } } public System.TimeSpan ShutdownTimeout { get { throw null; } set { } } + public System.TimeSpan StartupTimeout { get { throw null; } set { } } } } namespace Microsoft.Extensions.Hosting.Internal diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs index c6c29dc10cfb2..92d27878a9415 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.Threading; using Microsoft.Extensions.Configuration; namespace Microsoft.Extensions.Hosting @@ -13,10 +14,25 @@ namespace Microsoft.Extensions.Hosting public class HostOptions { /// - /// The default timeout for . + /// The default timeout for . /// + /// + /// This timeout also encompasses all host services implementing + /// and + /// . + /// public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30); + /// + /// The default timeout for . + /// + /// + /// This timeout also encompasses all host services implementing + /// and + /// . + /// + public TimeSpan StartupTimeout { get; set; } = Timeout.InfiniteTimeSpan; + /// /// Determines if the will start registered instances of concurrently or sequentially. Defaults to false. /// @@ -46,6 +62,13 @@ internal void Initialize(IConfiguration configuration) ShutdownTimeout = TimeSpan.FromSeconds(seconds); } + timeoutSeconds = configuration["startupTimeoutSeconds"]; + if (!string.IsNullOrEmpty(timeoutSeconds) + && int.TryParse(timeoutSeconds, NumberStyles.None, CultureInfo.InvariantCulture, out seconds)) + { + StartupTimeout = TimeSpan.FromSeconds(seconds); + } + var servicesStartConcurrently = configuration["servicesStartConcurrently"]; if (!string.IsNullOrEmpty(servicesStartConcurrently) && bool.TryParse(servicesStartConcurrently, out bool startBehavior)) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index 13b18f8e3cf15..c64f899fc4dfa 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -23,6 +23,7 @@ internal sealed class Host : IHost, IAsyncDisposable private readonly IHostEnvironment _hostEnvironment; private readonly PhysicalFileProvider _defaultProvider; private IEnumerable? _hostedServices; + private IEnumerable? _hostedLifecycleServices; private volatile bool _stopCalled; public Host(IServiceProvider services, @@ -54,95 +55,101 @@ public Host(IServiceProvider services, public IServiceProvider Services { get; } + /// + /// Order: + /// IHostLifetime.WaitForStartAsync + /// IHostedLifecycleService.StartingAsync + /// IHostedService.Start + /// IHostedLifecycleService.StartedAsync + /// IHostApplicationLifetime.ApplicationStarted + /// public async Task StartAsync(CancellationToken cancellationToken = default) { _logger.Starting(); - using var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping); - CancellationToken combinedCancellationToken = combinedCancellationTokenSource.Token; - - await _hostLifetime.WaitForStartAsync(combinedCancellationToken).ConfigureAwait(false); + CancellationTokenSource? cts = null; + CancellationTokenSource linkedCts; + if (_options.StartupTimeout != Timeout.InfiniteTimeSpan) + { + cts = new CancellationTokenSource(_options.StartupTimeout); + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken, _applicationLifetime.ApplicationStopping); + } + else + { + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping); + } - combinedCancellationToken.ThrowIfCancellationRequested(); - _hostedServices = Services.GetRequiredService>(); + using (cts) + using (linkedCts) + { + CancellationToken token = linkedCts.Token; - List exceptions = new List(); + // This may not catch exceptions. + await _hostLifetime.WaitForStartAsync(token).ConfigureAwait(false); + token.ThrowIfCancellationRequested(); - if (_options.ServicesStartConcurrently) - { - List tasks = new List(); + List exceptions = new(); + _hostedServices = Services.GetRequiredService>(); + _hostedLifecycleServices = GetHostLifecycles(_hostedServices); + bool concurrent = _options.ServicesStartConcurrently; + bool abortOnFirstException = !concurrent; - foreach (IHostedService hostedService in _hostedServices) + if (_hostedLifecycleServices is not null) { - tasks.Add(Task.Run(() => StartAndTryToExecuteAsync(hostedService, combinedCancellationToken), combinedCancellationToken)); + // Call StartingAsync(). + await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, + (service, token) => service.StartingAsync(token)).ConfigureAwait(false); } - Task groupedTasks = Task.WhenAll(tasks); + // Call StartAsync(). + await ForeachService(_hostedServices, token, concurrent, abortOnFirstException, exceptions, + async (service, token) => + { + await service.StartAsync(token).ConfigureAwait(false); - try - { - await groupedTasks.ConfigureAwait(false); - } - catch (Exception ex) + if (service is BackgroundService backgroundService) + { + _ = TryExecuteBackgroundServiceAsync(backgroundService); + } + }).ConfigureAwait(false); + + if (_hostedLifecycleServices is not null) { - exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable()); + // Call StartedAsync(). + await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, + (service, token) => service.StartedAsync(token)).ConfigureAwait(false); } - } - else - { - foreach (IHostedService hostedService in _hostedServices) + + if (exceptions.Count > 0) { - try + if (exceptions.Count == 1) { - // Fire IHostedService.Start - await StartAndTryToExecuteAsync(hostedService, combinedCancellationToken).ConfigureAwait(false); + // Rethrow if it's a single error + Exception singleException = exceptions[0]; + _logger.HostedServiceStartupFaulted(singleException); + ExceptionDispatchInfo.Capture(singleException).Throw(); } - catch (Exception ex) + else { - exceptions.Add(ex); - break; + var ex = new AggregateException("One or more hosted services failed to start.", exceptions); + _logger.HostedServiceStartupFaulted(ex); + throw ex; } } - } - if (exceptions.Count > 0) - { - if (exceptions.Count == 1) - { - // Rethrow if it's a single error - Exception singleException = exceptions[0]; - _logger.HostedServiceStartupFaulted(singleException); - ExceptionDispatchInfo.Capture(singleException).Throw(); - } - else - { - var ex = new AggregateException("One or more hosted services failed to start.", exceptions); - _logger.HostedServiceStartupFaulted(ex); - throw ex; - } + // Call IHostApplicationLifetime.Started + // This catches all exceptions and does not re-throw. + _applicationLifetime.NotifyStarted(); } - // Fire IHostApplicationLifetime.Started - _applicationLifetime.NotifyStarted(); - _logger.Started(); } - private async Task StartAndTryToExecuteAsync(IHostedService service, CancellationToken combinedCancellationToken) - { - await service.StartAsync(combinedCancellationToken).ConfigureAwait(false); - - if (service is BackgroundService backgroundService) - { - _ = TryExecuteBackgroundServiceAsync(backgroundService); - } - } - private async Task TryExecuteBackgroundServiceAsync(BackgroundService backgroundService) { // backgroundService.ExecuteTask may not be set (e.g. if the derived class doesn't call base.StartAsync) Task? backgroundTask = backgroundService.ExecuteTask; - if (backgroundTask == null) + if (backgroundTask is null) { return; } @@ -164,68 +171,87 @@ private async Task TryExecuteBackgroundServiceAsync(BackgroundService background if (_options.BackgroundServiceExceptionBehavior == BackgroundServiceExceptionBehavior.StopHost) { _logger.BackgroundServiceStoppingHost(ex); + + // This catches all exceptions and does not re-throw. _applicationLifetime.StopApplication(); } } } + /// + /// Order: + /// IHostedLifecycleService.StoppingAsync + /// IHostApplicationLifetime.ApplicationStopping + /// IHostedService.Stop + /// IHostedLifecycleService.StoppedAsync + /// IHostApplicationLifetime.ApplicationStopped + /// IHostLifetime.StopAsync + /// public async Task StopAsync(CancellationToken cancellationToken = default) { _stopCalled = true; _logger.Stopping(); - using (var cts = new CancellationTokenSource(_options.ShutdownTimeout)) - using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken)) + CancellationTokenSource? cts = null; + CancellationTokenSource linkedCts; + if (_options.ShutdownTimeout != Timeout.InfiniteTimeSpan) + { + cts = new CancellationTokenSource(_options.ShutdownTimeout); + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken); + } + else + { + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + } + + using (cts) + using (linkedCts) { CancellationToken token = linkedCts.Token; - // Trigger IHostApplicationLifetime.ApplicationStopping - _applicationLifetime.StopApplication(); - var exceptions = new List(); - if (_hostedServices != null) // Started? + List exceptions = new(); + if (_hostedServices is null) // Started? + { + + // Call IHostApplicationLifetime.ApplicationStopping. + // This catches all exceptions and does not re-throw. + _applicationLifetime.StopApplication(); + } + else { // Ensure hosted services are stopped in LIFO order - IEnumerable hostedServices = _hostedServices.Reverse(); + IEnumerable reversedServices = _hostedServices.Reverse(); + IEnumerable? reversedLifetimeServices = _hostedLifecycleServices?.Reverse(); + bool concurrent = _options.ServicesStopConcurrently; - if (_options.ServicesStopConcurrently) + // Call StoppingAsync(). + if (reversedLifetimeServices is not null) { - List tasks = new List(); + await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, + (service, token) => service.StoppingAsync(token)).ConfigureAwait(false); + } - foreach (IHostedService hostedService in hostedServices) - { - tasks.Add(Task.Run(() => hostedService.StopAsync(token), token)); - } + // Call IHostApplicationLifetime.ApplicationStopping. + // This catches all exceptions and does not re-throw. + _applicationLifetime.StopApplication(); - Task groupedTasks = Task.WhenAll(tasks); + // Call StopAsync(). + await ForeachService(reversedServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => + service.StopAsync(token)).ConfigureAwait(false); - try - { - await groupedTasks.ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable()); - } - } - else + if (reversedLifetimeServices is not null) { - foreach (IHostedService hostedService in hostedServices) - { - try - { - await hostedService.StopAsync(token).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - } + // Call StoppedAsync(). + await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => + service.StoppedAsync(token)).ConfigureAwait(false); } } - // Fire IHostApplicationLifetime.Stopped + // Call IHostApplicationLifetime.Stopped + // This catches all exceptions and does not re-throw. _applicationLifetime.NotifyStopped(); + // This may not catch exceptions, so we do it here. try { await _hostLifetime.StopAsync(token).ConfigureAwait(false); @@ -256,6 +282,98 @@ public async Task StopAsync(CancellationToken cancellationToken = default) _logger.Stopped(); } + private static async Task ForeachService( + IEnumerable services, + CancellationToken token, + bool concurrent, + bool abortOnFirstException, + List exceptions, + Func operation) + { + if (concurrent) + { + // The beginning synchronous portions of the implementations are run serially in registration order for + // performance since it is common to return Task.Completed as a noop. + // Any subsequent asynchronous portions are grouped together run concurrently. + List? tasks = null; + + foreach (T service in services) + { + Task task; + try + { + task = operation(service, token); + } + catch (Exception ex) + { + exceptions.Add(ex); // Log exception from sync method. + continue; + } + + if (task.IsCompleted) + { + if (task.Exception is not null) + { + exceptions.AddRange(task.Exception.InnerExceptions); // Log exception from async method. + } + } + else + { + tasks ??= new(); + tasks.Add(Task.Run(() => task, token)); + } + } + + if (tasks is not null) + { + Task groupedTasks = Task.WhenAll(tasks); + + try + { + await groupedTasks.ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable()); + } + } + } + else + { + foreach (T service in services) + { + try + { + await operation(service, token).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + if (abortOnFirstException) + { + return; + } + } + } + } + } + + private static List? GetHostLifecycles(IEnumerable hostedServices) + { + List? _result = null; + + foreach (IHostedService hostedService in hostedServices) + { + if (hostedService is IHostedLifecycleService service) + { + _result ??= new List(); + _result.Add(service); + } + } + + return _result; + } + public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); public async ValueTask DisposeAsync() diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DisposeTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DisposeTests.cs new file mode 100644 index 0000000000000..30e777ec753ea --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/DisposeTests.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Tests +{ + public partial class DisposeTests + { + public static IHostBuilder CreateHostBuilder(Action configure) + { + return new HostBuilder().ConfigureServices(configure); + } + + [Fact] + public void DisposeCalled_Interface() + { + var hostBuilder = CreateHostBuilder(services => + { + services.AddSingleton((sp) => new MyService()); + }); + + IMyService obj; + using (var host = hostBuilder.Build()) + { + obj = host.Services.GetService(); + } + + Assert.True(obj.IsDisposed); + } + + [Fact] + public void DisposeCalled_Class() + { + var hostBuilder = CreateHostBuilder(services => + { + services.AddSingleton(); + }); + + MyService obj; + using (var host = hostBuilder.Build()) + { + obj = host.Services.GetService(); + } + + Assert.True(obj.IsDisposed); + } + + [Fact] + public void DisposeNotCalled() + { + var hostBuilder = CreateHostBuilder(services => + { + services.AddSingleton(new MyService()); + }); + + MyService obj; + using (var host = hostBuilder.Build()) + { + obj = host.Services.GetService(); + } + + Assert.False(obj.IsDisposed); + } + + public interface IMyService : IDisposable + { + bool IsDisposed { get; } + } + + public class MyService : IMyService + { + private bool _isDisposed; + + public MyService() + { + } + + public bool IsDisposed => _isDisposed; + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs new file mode 100644 index 0000000000000..31692fe563ef2 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs @@ -0,0 +1,338 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Tests +{ + public partial class LifecycleTests + { + [Fact] + public async Task StartingConcurrently() + { + var hostBuilder = CreateHostBuilder(services => + { + services + .AddHostedService>() + .AddHostedService>() + .Configure(opts => opts.ServicesStartConcurrently = true); + }); + + using (IHost host = hostBuilder.Build()) + { + StartingTestClass.s_wait1.Wait(); + StartingTestClass.s_wait1.Wait(); + StartingTestClass.s_wait2.Wait(); + StartingTestClass.s_wait2.Wait(); + StartingTestClass.s_wait3.Wait(); + StartingTestClass.s_wait3.Wait(); + + Verify(0, 0, 0, 0, 0, 0); + + // Both run serially until the await. + Task start = host.StartAsync(); + Verify(1, 1, 0, 0, 0, 0); + await Task.Delay(s_superShortDelay); + Verify(1, 1, 0, 0, 0, 0); + + // Resume and check that both are not finished. + StartingTestClass.s_wait1.Release(); + await StartingTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 0, 0, 0); + + StartingTestClass.s_wait1.Release(); + await StartingTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 1, 0, 0); + + // Resume and verify they finish. + StartingTestClass.s_wait3.Release(); + StartingTestClass.s_wait3.Release(); + await start; + Verify(1, 1, 1, 1, 1, 1); + Assert.True(start.IsCompleted); + } + + void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2) + { + Assert.Equal(initial1, StartingTestClass.s_initialCount); + Assert.Equal(initial2, StartingTestClass.s_initialCount); + Assert.Equal(pause1, StartingTestClass.s_pauseCount); + Assert.Equal(pause2, StartingTestClass.s_pauseCount); + Assert.Equal(final1, StartingTestClass.s_finalCount); + Assert.Equal(final2, StartingTestClass.s_finalCount); + } + } + + private class StartingTestClass : IHostedLifecycleService + { + public static int s_initialCount = 0; + public static int s_pauseCount = 0; + public static int s_finalCount = 0; + public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1); + + public async Task StartingAsync(CancellationToken cancellationToken) + { + s_initialCount++; + await s_wait1.WaitAsync(); + s_pauseCount++; + s_wait2.Release(); + await s_wait3.WaitAsync(); + s_finalCount++; + } + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + [Fact] + public async Task StartConcurrently() + { + var hostBuilder = CreateHostBuilder(services => + { + services. + AddHostedService>(). + AddHostedService>(). + Configure(opts => opts.ServicesStartConcurrently = true); + }); + + using (IHost host = hostBuilder.Build()) + { + StartTestClass.s_wait1.Wait(); + StartTestClass.s_wait1.Wait(); + StartTestClass.s_wait2.Wait(); + StartTestClass.s_wait2.Wait(); + StartTestClass.s_wait3.Wait(); + StartTestClass.s_wait3.Wait(); + + Verify(0, 0, 0, 0, 0, 0); + + // Both run serially until the await. + Task start = host.StartAsync(); + Verify(1, 1, 0, 0, 0, 0); + Verify(1, 1, 0, 0, 0, 0); + + // Resume and check that both are not finished. + StartTestClass.s_wait1.Release(); + await StartTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 0, 0, 0); + + StartTestClass.s_wait1.Release(); + await StartTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 1, 0, 0); + + // Resume and verify they finish. + StartTestClass.s_wait3.Release(); + StartTestClass.s_wait3.Release(); + await start; + Verify(1, 1, 1, 1, 1, 1); + Assert.True(start.IsCompleted); + } + + void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2) + { + Assert.Equal(initial1, StartTestClass.s_initialCount); + Assert.Equal(initial2, StartTestClass.s_initialCount); + Assert.Equal(pause1, StartTestClass.s_pauseCount); + Assert.Equal(pause2, StartTestClass.s_pauseCount); + Assert.Equal(final1, StartTestClass.s_finalCount); + Assert.Equal(final2, StartTestClass.s_finalCount); + } + } + + private class StartTestClass : IHostedLifecycleService + { + public static int s_initialCount = 0; + public static int s_pauseCount = 0; + public static int s_finalCount = 0; + public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1); + + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public async Task StartAsync(CancellationToken cancellationToken) + { + s_initialCount++; + await s_wait1.WaitAsync(); + s_pauseCount++; + s_wait2.Release(); + await s_wait3.WaitAsync(); + s_finalCount++; + } + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + [Fact] + public async Task StartNonconcurrently() + { + var hostBuilder = CreateHostBuilder(services => + { + services. + AddHostedService>(). + AddHostedService>(). + Configure(opts => opts.ServicesStartConcurrently = false); + }); + + using (IHost host = hostBuilder.Build()) + { + StartNonconcurrentTestClass.s_wait.Wait(); + StartNonconcurrentTestClass.s_wait.Wait(); + + Verify(0, 0); + + // Both run serially. + Task start = host.StartAsync(); + Verify(1, 0); + await Task.Delay(s_superShortDelay); + Verify(1, 0); + + // Resume and verify they finish. + StartNonconcurrentTestClass.s_wait.Release(); + StartNonconcurrentTestClass.s_wait.Release(); + await start; + Verify(1, 1); + Assert.True(start.IsCompleted); + } + + void Verify(int count1, int count2) + { + Assert.Equal(count1, StartNonconcurrentTestClass.s_count); + Assert.Equal(count2, StartNonconcurrentTestClass.s_count); + } + } + + private class StartNonconcurrentTestClass : IHostedLifecycleService + { + public static int s_count = 0; + public static SemaphoreSlim? s_wait = new SemaphoreSlim(1); + + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public async Task StartAsync(CancellationToken cancellationToken) + { + s_count++; + await s_wait.WaitAsync(); + } + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + [Fact] + public async Task StartedConcurrently() + { + var hostBuilder = CreateHostBuilder(services => + { + services + .AddHostedService>() + .AddHostedService>() + .Configure(opts => opts.ServicesStartConcurrently = true); + }); + + using (IHost host = hostBuilder.Build()) + { + StartedTestClass.s_wait1.Wait(); + StartedTestClass.s_wait1.Wait(); + StartedTestClass.s_wait2.Wait(); + StartedTestClass.s_wait2.Wait(); + StartedTestClass.s_wait3.Wait(); + StartedTestClass.s_wait3.Wait(); + + Verify(0, 0, 0, 0, 0, 0); + + // Both run serially until the await. + Task start = host.StartAsync(); + Verify(1, 1, 0, 0, 0, 0); + await Task.Delay(s_superShortDelay); + Verify(1, 1, 0, 0, 0, 0); + + // Resume and check that both are not finished. + StartedTestClass.s_wait1.Release(); + await StartedTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 0, 0, 0); + + StartedTestClass.s_wait1.Release(); + await StartedTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 1, 0, 0); + + // Resume and verify they finish. + StartedTestClass.s_wait3.Release(); + StartedTestClass.s_wait3.Release(); + await start; + Verify(1, 1, 1, 1, 1, 1); + Assert.True(start.IsCompleted); + } + + void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2) + { + Assert.Equal(initial1, StartedTestClass.s_initialCount); + Assert.Equal(initial2, StartedTestClass.s_initialCount); + Assert.Equal(pause1, StartedTestClass.s_pauseCount); + Assert.Equal(pause2, StartedTestClass.s_pauseCount); + Assert.Equal(final1, StartedTestClass.s_finalCount); + Assert.Equal(final2, StartedTestClass.s_finalCount); + } + } + + private class StartedTestClass : IHostedLifecycleService + { + public static int s_initialCount = 0; + public static int s_pauseCount = 0; + public static int s_finalCount = 0; + public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1); + + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public async Task StartedAsync(CancellationToken cancellationToken) + { + s_initialCount++; + await s_wait1.WaitAsync(); + s_pauseCount++; + s_wait2.Release(); + await s_wait3.WaitAsync(); + s_finalCount++; + } + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task StartPhasesException(bool throwAfterAsyncCall) + { + ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, throwOnStartup: true, throwOnShutdown: false); + var hostBuilder = CreateHostBuilder(services => + { + services.AddHostedService((token) => impl); + }); + + using (IHost host = hostBuilder.Build()) + { + AggregateException ex = await Assert.ThrowsAnyAsync(async () => await host.StartAsync()); + + Assert.True(impl.StartingCalled); + Assert.True(impl.StartCalled); + Assert.True(impl.StartedCalled); + + Assert.Equal(3, ex.InnerExceptions.Count); + Assert.Contains("(ThrowOnStarting)", ex.InnerExceptions[0].Message); + Assert.Contains("(ThrowOnStart)", ex.InnerExceptions[1].Message); + Assert.Contains("(ThrowOnStarted)", ex.InnerExceptions[2].Message); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs new file mode 100644 index 0000000000000..2b413d29be3b2 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs @@ -0,0 +1,350 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Tests +{ + public partial class LifecycleTests + { + [Fact] + public async Task StoppingConcurrently() + { + var hostBuilder = CreateHostBuilder(services => + { + services + .AddHostedService>() + .AddHostedService>() + .Configure(opts => opts.ServicesStopConcurrently = true); + }); + + using (IHost host = hostBuilder.Build()) + { + StoppingTestClass.s_wait1.Wait(); + StoppingTestClass.s_wait1.Wait(); + StoppingTestClass.s_wait2.Wait(); + StoppingTestClass.s_wait2.Wait(); + StoppingTestClass.s_wait3.Wait(); + StoppingTestClass.s_wait3.Wait(); + + await host.StartAsync(); + Verify(0, 0, 0, 0, 0, 0); + + // Both run serially until the await. + Task stop = host.StopAsync(); + Verify(1, 1, 0, 0, 0, 0); + await Task.Delay(s_superShortDelay); + Verify(1, 1, 0, 0, 0, 0); + + // Resume and check that both are not finished. + StoppingTestClass.s_wait1.Release(); + await StoppingTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 0, 0, 0); + + StoppingTestClass.s_wait1.Release(); + await StoppingTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 1, 0, 0); + + // Resume and verify they finish. + StoppingTestClass.s_wait3.Release(); + StoppingTestClass.s_wait3.Release(); + await stop; + Verify(1, 1, 1, 1, 1, 1); + Assert.True(stop.IsCompleted); + } + + void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2) + { + Assert.Equal(initial1, StoppingTestClass.s_initialCount); + Assert.Equal(initial2, StoppingTestClass.s_initialCount); + Assert.Equal(pause1, StoppingTestClass.s_pauseCount); + Assert.Equal(pause2, StoppingTestClass.s_pauseCount); + Assert.Equal(final1, StoppingTestClass.s_finalCount); + Assert.Equal(final2, StoppingTestClass.s_finalCount); + } + } + + private class StoppingTestClass : IHostedLifecycleService + { + public static int s_initialCount = 0; + public static int s_pauseCount = 0; + public static int s_finalCount = 0; + public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1); + + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public async Task StoppingAsync(CancellationToken cancellationToken) + { + s_initialCount++; + await s_wait1.WaitAsync(); + s_pauseCount++; + s_wait2.Release(); + await s_wait3.WaitAsync(); + s_finalCount++; + } + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + [Fact] + public async Task StopConcurrently() + { + var hostBuilder = CreateHostBuilder(services => + { + services. + AddHostedService>(). + AddHostedService>(). + Configure(opts => opts.ServicesStopConcurrently = true); + }); + + using (IHost host = hostBuilder.Build()) + { + StopTestClass.s_wait1.Wait(); + StopTestClass.s_wait1.Wait(); + StopTestClass.s_wait2.Wait(); + StopTestClass.s_wait2.Wait(); + StopTestClass.s_wait3.Wait(); + StopTestClass.s_wait3.Wait(); + + await host.StartAsync(); + Verify(0, 0, 0, 0, 0, 0); + + // Both run serially until the await. + Task stop = host.StopAsync(); + Verify(1, 1, 0, 0, 0, 0); + await Task.Delay(s_superShortDelay); + Verify(1, 1, 0, 0, 0, 0); + + // Resume and check that both are not finished. + StopTestClass.s_wait1.Release(); + await StopTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 0, 0, 0); + + StopTestClass.s_wait1.Release(); + await StopTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 1, 0, 0); + + // Resume and verify they finish. + StopTestClass.s_wait3.Release(); + StopTestClass.s_wait3.Release(); + await stop; + Verify(1, 1, 1, 1, 1, 1); + Assert.True(stop.IsCompleted); + } + + void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2) + { + Assert.Equal(initial1, StopTestClass.s_initialCount); + Assert.Equal(initial2, StopTestClass.s_initialCount); + Assert.Equal(pause1, StopTestClass.s_pauseCount); + Assert.Equal(pause2, StopTestClass.s_pauseCount); + Assert.Equal(final1, StopTestClass.s_finalCount); + Assert.Equal(final2, StopTestClass.s_finalCount); + } + } + + private class StopTestClass : IHostedLifecycleService + { + public static int s_initialCount = 0; + public static int s_pauseCount = 0; + public static int s_finalCount = 0; + public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1); + + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public async Task StopAsync(CancellationToken cancellationToken) + { + s_initialCount++; + await s_wait1.WaitAsync(); + s_pauseCount++; + s_wait2.Release(); + await s_wait3.WaitAsync(); + s_finalCount++; + } + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + [Fact] + public async Task StopNonconcurrently() + { + var hostBuilder = CreateHostBuilder(services => + { + services. + AddHostedService>(). + AddHostedService>(). + Configure(opts => opts.ServicesStopConcurrently = false); + }); + + using (IHost host = hostBuilder.Build()) + { + StopNonconcurrentTestClass.s_wait.Wait(); + StopNonconcurrentTestClass.s_wait.Wait(); + + await host.StartAsync(); + Verify(0, 0); + + // Both run serially in reverse order. + Task stop = host.StopAsync(); + Verify(0, 1); + await Task.Delay(s_superShortDelay); + Verify(0, 1); + + // Resume and verify they finish. + StopNonconcurrentTestClass.s_wait.Release(); + StopNonconcurrentTestClass.s_wait.Release(); + await stop; + Verify(1, 1); + Assert.True(stop.IsCompleted); + } + + void Verify(int count1, int count2) + { + Assert.Equal(count1, StopNonconcurrentTestClass.s_count); + Assert.Equal(count2, StopNonconcurrentTestClass.s_count); + } + } + + private class StopNonconcurrentTestClass : IHostedLifecycleService + { + public static int s_count = 0; + public static SemaphoreSlim? s_wait = new SemaphoreSlim(1); + + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public async Task StopAsync(CancellationToken cancellationToken) + { + s_count++; + await s_wait.WaitAsync(); + } + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + [Fact] + public async Task StoppedConcurrently() + { + var hostBuilder = CreateHostBuilder(services => + { + services + .AddHostedService>() + .AddHostedService>() + .Configure(opts => opts.ServicesStopConcurrently = true); + }); + + using (IHost host = hostBuilder.Build()) + { + StoppedTestClass.s_wait1.Wait(); + StoppedTestClass.s_wait1.Wait(); + StoppedTestClass.s_wait2.Wait(); + StoppedTestClass.s_wait2.Wait(); + StoppedTestClass.s_wait3.Wait(); + StoppedTestClass.s_wait3.Wait(); + + await host.StartAsync(); + Verify(0, 0, 0, 0, 0, 0); + + // Both run serially until the await. + Task stop = host.StopAsync(); + Verify(1, 1, 0, 0, 0, 0); + await Task.Delay(s_superShortDelay); + Verify(1, 1, 0, 0, 0, 0); + + // Resume and check that both are not finished. + StoppedTestClass.s_wait1.Release(); + await StoppedTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 0, 0, 0); + + StoppedTestClass.s_wait1.Release(); + await StoppedTestClass.s_wait2.WaitAsync(); + Verify(1, 1, 1, 1, 0, 0); + + // Resume and verify they finish. + StoppedTestClass.s_wait3.Release(); + StoppedTestClass.s_wait3.Release(); + await stop; + Verify(1, 1, 1, 1, 1, 1); + Assert.True(stop.IsCompleted); + } + + void Verify(int initial1, int initial2, int pause1, int pause2, int final1, int final2) + { + Assert.Equal(initial1, StoppedTestClass.s_initialCount); + Assert.Equal(initial2, StoppedTestClass.s_initialCount); + Assert.Equal(pause1, StoppedTestClass.s_pauseCount); + Assert.Equal(pause2, StoppedTestClass.s_pauseCount); + Assert.Equal(final1, StoppedTestClass.s_finalCount); + Assert.Equal(final2, StoppedTestClass.s_finalCount); + } + } + + private class StoppedTestClass : IHostedLifecycleService + { + public static int s_initialCount = 0; + public static int s_pauseCount = 0; + public static int s_finalCount = 0; + public static SemaphoreSlim? s_wait1 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait2 = new SemaphoreSlim(1); + public static SemaphoreSlim? s_wait3 = new SemaphoreSlim(1); + + public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public async Task StoppedAsync(CancellationToken cancellationToken) + { + s_initialCount++; + await s_wait1.WaitAsync(); + s_pauseCount++; + s_wait2.Release(); + await s_wait3.WaitAsync(); + s_finalCount++; + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task StopPhasesException(bool throwAfterAsyncCall) + { + ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, throwOnStartup: false, throwOnShutdown: true); + var hostBuilder = CreateHostBuilder(services => + { + services.AddHostedService((token) => impl); + }); + + using (IHost host = hostBuilder.Build()) + { + await host.StartAsync(); + AggregateException ex = await Assert.ThrowsAnyAsync(async () => await host.StopAsync()); + + Assert.True(impl.StartingCalled); + Assert.True(impl.StartCalled); + Assert.True(impl.StartedCalled); + + // An exception during a stop phase does not prevent the next ones from running. + Assert.True(impl.StoppingCalled); + Assert.True(impl.StopCalled); + Assert.True(impl.StoppedCalled); + + Assert.Equal(3, ex.InnerExceptions.Count); + Assert.Contains("(ThrowOnStopping)", ex.InnerExceptions[0].Message); + Assert.Contains("(ThrowOnStop)", ex.InnerExceptions[1].Message); + Assert.Contains("(ThrowOnStopped)", ex.InnerExceptions[2].Message); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Timeouts.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Timeouts.cs new file mode 100644 index 0000000000000..bf215b490f44d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Timeouts.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Tests +{ + public partial class LifecycleTests + { + [OuterLoop("Uses Task.Delay")] + [Theory] + [InlineData(TimeoutService.Phase.Starting)] + [InlineData(TimeoutService.Phase.Start)] + [InlineData(TimeoutService.Phase.Started)] + public async Task StartTimeoutClass_WithValue(TimeoutService.Phase phase) + { + var host = new HostBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddHostedService((token) => new TimeoutService(phase)); + services.Configure((opts) => + { + opts.StartupTimeout = s_shortDelay; + }); + }) + .UseConsoleLifetime() + .Build(); + + await Assert.ThrowsAsync(async () => await host.StartAsync()); + } + + [OuterLoop("Uses Task.Delay")] + [Theory] + [InlineData(TimeoutService.Phase.Start)] + public async Task StartTimeoutClass_WithValue_Concurrently(TimeoutService.Phase phase) + { + var host = new HostBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddHostedService((token) => new TimeoutService(phase)); + services.Configure((opts) => + { + opts.StartupTimeout = s_shortDelay; + opts.ServicesStartConcurrently = true; + }); + }) + .UseConsoleLifetime() + .Build(); + + await Assert.ThrowsAsync(async () => await host.StartAsync()); + } + + [OuterLoop("Uses Task.Delay")] + [Theory] + [InlineData(TimeoutService.Phase.Stopping)] + [InlineData(TimeoutService.Phase.Stop)] + [InlineData(TimeoutService.Phase.Stopped)] + public async Task StopTimeoutClass_WithValue(TimeoutService.Phase phase) + { + var host = new HostBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddHostedService((token) => new TimeoutService(phase)); + services.Configure((opts) => + { + opts.ShutdownTimeout = s_shortDelay; + }); + }) + .UseConsoleLifetime() + .Build(); + + await host.StartAsync(); + await Assert.ThrowsAsync(async () => await host.StopAsync()); + } + + [OuterLoop("Uses Task.Delay")] + [Theory] + [InlineData(TimeoutService.Phase.Stop)] + public async Task StopTimeoutClass_WithValue_Concurrently(TimeoutService.Phase phase) + { + var host = new HostBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddHostedService((token) => new TimeoutService(phase)); + services.Configure((opts) => + { + opts.ShutdownTimeout = s_shortDelay; + opts.ServicesStopConcurrently = true; + }); + }) + .UseConsoleLifetime() + .Build(); + + await host.StartAsync(); + await Assert.ThrowsAsync(async () => await host.StopAsync()); + } + + public class TimeoutService : IHostedLifecycleService + { + private Phase _phase; + + public TimeoutService(Phase phase) + { + _phase = phase; + } + + public async Task StartingAsync(CancellationToken cancellationToken) + { + if (_phase == Phase.Starting) + { + await Task.Delay(s_longDelay, cancellationToken); + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + if (_phase == Phase.Start) + { + await Task.Delay(s_longDelay, cancellationToken); + } + } + + public async Task StartedAsync(CancellationToken cancellationToken) + { + if (_phase == Phase.Started) + { + await Task.Delay(s_longDelay, cancellationToken); + } + } + + public async Task StoppingAsync(CancellationToken cancellationToken) + { + if (_phase == Phase.Stopping) + { + await Task.Delay(s_longDelay, cancellationToken); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (_phase == Phase.Stop) + { + await Task.Delay(s_longDelay, cancellationToken); + } + } + + public async Task StoppedAsync(CancellationToken cancellationToken) + { + if (_phase == Phase.Stopped) + { + await Task.Delay(s_longDelay, cancellationToken); + } + } + + public enum Phase + { + Starting, + Start, + Started, + Stopping, + Stop, + Stopped + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs new file mode 100644 index 0000000000000..373913cc3de6b --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs @@ -0,0 +1,285 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.Hosting.Tests +{ + public partial class LifecycleTests + { + private static TimeSpan s_superShortDelay = TimeSpan.FromSeconds(.05); + + // Tests that actually delay this long should be [OuterLoop]: + private static TimeSpan s_shortDelay = TimeSpan.FromSeconds(.5); + private static TimeSpan s_longDelay = TimeSpan.FromSeconds(5); + + public static IHostBuilder CreateHostBuilder(Action configure) => + new HostBuilder().ConfigureServices(configure); + + [Fact] + public async Task HostedService_CallbackOccursOnce() + { + var hostBuilder = CreateHostBuilder(services => + { + services.AddHostedService(); + services.AddHostedService(); + }); + + using (IHost host = hostBuilder.Build()) + { + await host.StartAsync(); + } + + Assert.Equal(1, HostedService_CallbackOccursOnce_Impl.s_callbackCallCount); + } + + private class HostedService_CallbackOccursOnce_Impl : IHostedService + { + public static int s_callbackCallCount = 0; + + public Task StartAsync(CancellationToken cancellationToken) + { + s_callbackCallCount++; + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CallbackOrder(bool concurrently) + { + var hostBuilder = CreateHostBuilder(services => + { + services + .AddHostedService() + .AddSingleton((sp) => sp.GetServices().OfType().First()) + .AddSingleton((sp) => sp.GetServices().OfType().First()) + .Configure(opts => opts.ServicesStartConcurrently = concurrently) + .Configure(opts => opts.ServicesStopConcurrently = concurrently); + }); + using (IHost host = hostBuilder.Build()) + { + CallbackOrder_Impl impl = host.Services.GetService(); + + await host.StartAsync(); + await host.StopAsync(); + + Assert.Equal(1, impl._hostWaitForStartAsyncOrder); + Assert.Equal(2, impl._startingOrder); + Assert.Equal(3, impl._startOrder); + Assert.Equal(4, impl._startedOrder); + Assert.Equal(5, impl._applicationStartedOrder); + Assert.Equal(6, impl._stoppingOrder); + Assert.Equal(7, impl._applicationStoppingOrder); + Assert.Equal(8, impl._stopOrder); + Assert.Equal(9, impl._stoppedOrder); + Assert.Equal(10, impl._applicationStoppedOrder); + Assert.Equal(11, impl._hostStoppedOrder); + } + } + + private class CallbackOrder_Impl : IHostedLifecycleService, IHostLifetime + { + public int _hostWaitForStartAsyncOrder; + public int _startingOrder; + public int _startOrder; + public int _startedOrder; + public int _applicationStartedOrder; + public int _stoppingOrder; + public int _applicationStoppingOrder; + public int _stopOrder; + public int _stoppedOrder; + public int _applicationStoppedOrder; + public int _hostStoppedOrder; + + private int _callCount; + + public CallbackOrder_Impl(IServiceProvider provider) + { + IHostApplicationLifetime lifetime = provider.GetService(); + + lifetime.ApplicationStarted.Register(() => + { + _applicationStartedOrder = ++_callCount; + }); + + lifetime.ApplicationStopping.Register(() => + { + _applicationStoppingOrder = ++_callCount; + }); + + lifetime.ApplicationStopped.Register(() => + { + _applicationStoppedOrder = ++_callCount; + }); + } + + Task IHostLifetime.WaitForStartAsync(CancellationToken cancellationToken) + { + _hostWaitForStartAsyncOrder = ++_callCount; + return Task.CompletedTask; + } + + public Task StartingAsync(CancellationToken cancellationToken) + { + _startingOrder = ++_callCount; + return Task.CompletedTask; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _startOrder = ++_callCount; + return Task.CompletedTask; + } + + public Task StartedAsync(CancellationToken cancellationToken) + { + _startedOrder = ++_callCount; + return Task.CompletedTask; + } + + public Task StoppingAsync(CancellationToken cancellationToken) + { + _stoppingOrder = ++_callCount; + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _stopOrder = ++_callCount; + return Task.CompletedTask; + } + public Task StoppedAsync(CancellationToken cancellationToken) + { + _stoppedOrder = ++_callCount; + return Task.CompletedTask; + } + + Task IHostLifetime.StopAsync(System.Threading.CancellationToken cancellationToken) + { + _hostStoppedOrder = ++_callCount; + return Task.CompletedTask; + } + } + + private class ExceptionImpl : IHostedLifecycleService + { + private bool _throwAfterAsyncCall; + public bool StartingCalled = false; + public bool StartCalled = false; + public bool StartedCalled = false; + public bool StoppingCalled = false; + public bool StopCalled = false; + public bool StoppedCalled = false; + + public bool ThrowOnStartup; + public bool ThrowOnShutdown; + + public ExceptionImpl( + bool throwAfterAsyncCall, + bool throwOnStartup, + bool throwOnShutdown) + { + _throwAfterAsyncCall = throwAfterAsyncCall; + ThrowOnStartup = throwOnStartup; + ThrowOnShutdown = throwOnShutdown; + } + + public async Task StartingAsync(CancellationToken cancellationToken) + { + StartingCalled = true; + if (ThrowOnStartup) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStarting)"); + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + StartCalled = true; + if (ThrowOnStartup) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStart)"); + } + } + + public async Task StartedAsync(CancellationToken cancellationToken) + { + StartedCalled = true; + if (ThrowOnStartup) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStarted)"); + } + } + + public async Task StoppingAsync(CancellationToken cancellationToken) + { + StoppingCalled = true; + if (ThrowOnShutdown) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStopping)"); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + StopCalled = true; + if (ThrowOnShutdown) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStop)"); + } + } + + public async Task StoppedAsync(CancellationToken cancellationToken) + { + StoppedCalled = true; + if (ThrowOnShutdown) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStopped)"); + } + } + } + + // These are used to close open generic types: + private sealed class Impl1 { } + private sealed class Impl2 { } + } +}