From bc70792690937e88457ec3e0c771a48a1668ac30 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Fri, 9 Jun 2023 13:29:29 -0500 Subject: [PATCH 1/5] Add IHostedLifecycleService commit d0e9189a6fe21b3e604dc3188a804d0cb252125d Author: Steve Harter Date: Fri Jun 9 13:20:30 2023 -0500 sync commit a5e88d58c15be5f3fe63e4d3f994448054ca8dbe Merge: 8010ffa392c 9b7db0f4436 Author: Steve Harter Date: Fri Jun 9 10:59:47 2023 -0500 Merge branch 'main' of https://github.com/steveharter/runtime into Startup commit 8010ffa392cc10ceb96ed84c7a1c571a7dc47aac Author: Steve Harter Date: Fri May 19 10:39:31 2023 -0500 Add IServiceStartup to enable pre-startup hooks --- ...crosoft.Extensions.Hosting.Abstractions.cs | 7 + .../src/IHostedLifecycleService.cs | 44 ++ .../ref/Microsoft.Extensions.Hosting.cs | 1 + .../src/HostOptions.cs | 25 +- .../src/Internal/Host.cs | 431 +++++++++++++++--- .../tests/UnitTests/DisposeTests.cs | 99 ++++ .../tests/UnitTests/LifecycleTests.Start.cs | 341 ++++++++++++++ .../tests/UnitTests/LifecycleTests.Stop.cs | 352 ++++++++++++++ .../UnitTests/LifecycleTests.Timeouts.cs | 169 +++++++ .../tests/UnitTests/LifecycleTests.cs | 177 +++++++ 10 files changed, 1573 insertions(+), 73 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..2f98128525e96 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, @@ -58,79 +59,87 @@ 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); - - combinedCancellationToken.ThrowIfCancellationRequested(); - _hostedServices = Services.GetRequiredService>(); - - List exceptions = new List(); - - if (_options.ServicesStartConcurrently) + CancellationTokenSource? cts = null; + CancellationTokenSource linkedCts; + if (_options.StartupTimeout != Timeout.InfiniteTimeSpan) { - List tasks = new List(); + cts = new CancellationTokenSource(_options.StartupTimeout); + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken, _applicationLifetime.ApplicationStopping); + } + else + { + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _applicationLifetime.ApplicationStopping); + } - foreach (IHostedService hostedService in _hostedServices) - { - tasks.Add(Task.Run(() => StartAndTryToExecuteAsync(hostedService, combinedCancellationToken), combinedCancellationToken)); - } + using (cts) + using (linkedCts) + { + CancellationToken token = linkedCts.Token; + List exceptions = new(); + _hostedServices = Services.GetRequiredService>(); + _hostedLifecycleServices = GetHostLifecycles(_hostedServices); - Task groupedTasks = Task.WhenAll(tasks); + await _hostLifetime.WaitForStartAsync(token).ConfigureAwait(false); + token.ThrowIfCancellationRequested(); - try + if (_hostedLifecycleServices is not null) { - await groupedTasks.ConfigureAwait(false); + await CallLifeCycle_Starting(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false); } - catch (Exception ex) + + // Run IHostedService.Start either concurrently or serially. + if (_options.ServicesStartConcurrently) { - exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable()); + await CallLifeCycle_Start(_hostedServices, token, exceptions).ConfigureAwait(false); } - } - else - { - foreach (IHostedService hostedService in _hostedServices) + else { - try + foreach (IHostedService hostedService in _hostedServices) { - // Fire IHostedService.Start - await StartAndTryToExecuteAsync(hostedService, combinedCancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); - break; + try + { + await StartAndTryToExecuteAsync(hostedService, token).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + break; + } } } - } - if (exceptions.Count > 0) - { - if (exceptions.Count == 1) + if (_hostedLifecycleServices is not null) { - // Rethrow if it's a single error - Exception singleException = exceptions[0]; - _logger.HostedServiceStartupFaulted(singleException); - ExceptionDispatchInfo.Capture(singleException).Throw(); + await CallLifeCycle_Started(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false); } - else + + if (exceptions.Count > 0) { - var ex = new AggregateException("One or more hosted services failed to start.", exceptions); - _logger.HostedServiceStartupFaulted(ex); - throw ex; + 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; + } } - } - // Fire IHostApplicationLifetime.Started - _applicationLifetime.NotifyStarted(); + // Fire IHostApplicationLifetime.Started + _applicationLifetime.NotifyStarted(); + } _logger.Started(); } - private async Task StartAndTryToExecuteAsync(IHostedService service, CancellationToken combinedCancellationToken) + private async Task StartAndTryToExecuteAsync(IHostedService service, CancellationToken token) { - await service.StartAsync(combinedCancellationToken).ConfigureAwait(false); + await service.StartAsync(token).ConfigureAwait(false); if (service is BackgroundService backgroundService) { @@ -174,42 +183,46 @@ 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; + List exceptions = new(); + // Trigger IHostApplicationLifetime.ApplicationStopping _applicationLifetime.StopApplication(); - var exceptions = new List(); if (_hostedServices != null) // Started? { // Ensure hosted services are stopped in LIFO order - IEnumerable hostedServices = _hostedServices.Reverse(); + IEnumerable reversedServices = _hostedServices.Reverse(); + IEnumerable? reversedLifetimeServices = _hostedLifecycleServices?.Reverse(); - if (_options.ServicesStopConcurrently) + if (reversedLifetimeServices is not null) { - List tasks = new List(); - - foreach (IHostedService hostedService in hostedServices) - { - tasks.Add(Task.Run(() => hostedService.StopAsync(token), token)); - } - - Task groupedTasks = Task.WhenAll(tasks); + await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false); + } - try - { - await groupedTasks.ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable()); - } + // Run IHostedService.Stop either concurrently or serially. + if (_options.ServicesStopConcurrently) + { + await CallLifeCycle_Stop(reversedServices, token, exceptions).ConfigureAwait(false); } else { - foreach (IHostedService hostedService in hostedServices) + foreach (IHostedService hostedService in reversedServices) { try { @@ -221,6 +234,11 @@ public async Task StopAsync(CancellationToken cancellationToken = default) } } } + + if (reversedLifetimeServices is not null) + { + await CallLifeCycle_Stopped(reversedLifetimeServices, token, exceptions).ConfigureAwait(false); + } } // Fire IHostApplicationLifetime.Stopped @@ -256,6 +274,275 @@ public async Task StopAsync(CancellationToken cancellationToken = default) _logger.Stopped(); } + /// + /// Call . + /// 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. + /// + private static async Task CallLifeCycle_Starting(IEnumerable services, CancellationToken token, List exceptions) + { + List? tasks = null; + + foreach (IHostedLifecycleService service in services) + { + Task? task = task = service.StartingAsync(token); + if (task.IsCompleted) + { + if (task.Exception is not null) + { + exceptions.Add(task.Exception); + } + } + else + { + tasks ??= new(); + tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); + } + } + } + + /// + /// Call . + /// 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. + /// + private static async Task CallLifeCycle_Start(IEnumerable services, CancellationToken token, List exceptions) + { + List? tasks = null; + + foreach (IHostedService service in services) + { + Task? task = service.StartAsync(token); + if (task.IsCompleted) + { + if (task.Exception is not null) + { + exceptions.Add(task.Exception); + } + } + else + { + tasks ??= new(); + tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); + } + } + } + + /// + /// Call . + /// 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. + /// + private static async Task CallLifeCycle_Started(IEnumerable services, CancellationToken token, List exceptions) + { + List? tasks = null; + + foreach (IHostedLifecycleService service in services) + { + Task? task = task = service.StartedAsync(token); + if (task.IsCompleted) + { + if (task.Exception is not null) + { + exceptions.Add(task.Exception); + } + } + else + { + tasks ??= new(); + tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); + } + } + } + + /// + /// Call . + /// 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. + /// + private static async Task CallLifeCycle_Stopping(IEnumerable services, CancellationToken token, List exceptions) + { + List? tasks = null; + + foreach (IHostedLifecycleService service in services) + { + Task? task = task = service.StoppingAsync(token); + if (task.IsCompleted) + { + if (task.Exception is not null) + { + exceptions.Add(task.Exception); + } + } + else + { + tasks ??= new(); + tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); + } + } + } + + /// + /// Call . + /// 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. + /// + private static async Task CallLifeCycle_Stop(IEnumerable services, CancellationToken token, List exceptions) + { + List? tasks = null; + + foreach (IHostedService service in services) + { + Task? task = task = service.StopAsync(token); + if (task.IsCompleted) + { + if (task.Exception is not null) + { + exceptions.Add(task.Exception); + } + } + else + { + tasks ??= new(); + tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); + } + } + } + + /// + /// Call . + /// 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. + /// + private static async Task CallLifeCycle_Stopped(IEnumerable services, CancellationToken token, List exceptions) + { + List? tasks = null; + + foreach (IHostedLifecycleService service in services) + { + Task? task = task = service.StoppedAsync(token); + if (task.IsCompleted) + { + if (task.Exception is not null) + { + exceptions.Add(task.Exception); + } + } + else + { + tasks ??= new(); + tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); + } + } + } + + 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..61d5ce9583c65 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs @@ -0,0 +1,341 @@ +// 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>(); + }); + + 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); + await Task.Delay(s_superShortDelay); + 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>(); + }); + + 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, true, true, true, false, false, 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.Contains("(ThrowOnStarting)", ex.Message); + Assert.Contains("(ThrowOnStart)", ex.Message); + Assert.Contains("(ThrowOnStarted)", ex.Message); + + 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..c85d9bc26048f --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs @@ -0,0 +1,352 @@ +// 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>(); + }); + + 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>(); + }); + + 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, false, false, false, true, true, 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.Contains("(ThrowOnStopping)", ex.Message); + Assert.Contains("(ThrowOnStop)", ex.Message); + Assert.Contains("(ThrowOnStopped)", ex.Message); + + 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..518f8b364bb89 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs @@ -0,0 +1,177 @@ +// 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 + { + 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; + } + + 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 ThrowOnStarting; + public bool ThrowOnStart; + public bool ThrowOnStarted; + public bool ThrowOnStopping; + public bool ThrowOnStop; + public bool ThrowOnStopped; + + public ExceptionImpl( + bool throwAfterAsyncCall, + bool throwOnStarting, + bool throwOnStart, + bool throwOnStarted, + bool throwOnStopping, + bool throwOnStop, + bool throwOnStopped) + { + _throwAfterAsyncCall = throwAfterAsyncCall; + ThrowOnStarting = throwOnStarting; + ThrowOnStart = throwOnStart; + ThrowOnStarted = throwOnStarted; + ThrowOnStopping = throwOnStopping; + ThrowOnStop = throwOnStop; + ThrowOnStopped = throwOnStopped; + } + + public async Task StartingAsync(CancellationToken cancellationToken) + { + StartingCalled = true; + if (ThrowOnStarting) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStarting)"); + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + StartCalled = true; + if (ThrowOnStart) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStart)"); + } + } + + public async Task StartedAsync(CancellationToken cancellationToken) + { + StartedCalled = true; + if (ThrowOnStarted) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStarted)"); + } + } + + public async Task StoppingAsync(CancellationToken cancellationToken) + { + StoppingCalled = true; + if (ThrowOnStopping) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStopping)"); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + StopCalled = true; + if (ThrowOnStop) + { + if (_throwAfterAsyncCall) + { + await Task.Delay(s_superShortDelay); + } + + throw new Exception("(ThrowOnStop)"); + } + } + + public async Task StoppedAsync(CancellationToken cancellationToken) + { + StoppedCalled = true; + if (ThrowOnStopped) + { + 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 { } + } +} From 73d5854cb304a11e152e6a3b4217cf969ceffa1f Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Thu, 29 Jun 2023 11:23:47 -0500 Subject: [PATCH 2/5] Use ServicesConcurrently knobs for new callbacks --- .../src/Internal/Host.cs | 270 ++++++++++++++---- .../tests/UnitTests/LifecycleTests.Start.cs | 8 +- .../tests/UnitTests/LifecycleTests.Stop.cs | 8 +- .../tests/UnitTests/LifecycleTests.cs | 139 +++++++-- 4 files changed, 338 insertions(+), 87 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index 2f98128525e96..9623fb333e04d 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -55,6 +55,13 @@ public Host(IServiceProvider services, public IServiceProvider Services { get; } + /// + /// Order: + /// IHostedLifecycleService.StartingAsync + /// IHostedService.Start + /// IHostedLifecycleService.StartedAsync + /// IHostApplicationLifetime.ApplicationStarted + /// public async Task StartAsync(CancellationToken cancellationToken = default) { _logger.Starting(); @@ -82,18 +89,39 @@ public async Task StartAsync(CancellationToken cancellationToken = default) await _hostLifetime.WaitForStartAsync(token).ConfigureAwait(false); token.ThrowIfCancellationRequested(); - if (_hostedLifecycleServices is not null) - { - await CallLifeCycle_Starting(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false); - } - // Run IHostedService.Start either concurrently or serially. if (_options.ServicesStartConcurrently) { + if (_hostedLifecycleServices is not null) + { + await CallLifeCycle_Starting(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false); + } + await CallLifeCycle_Start(_hostedServices, token, exceptions).ConfigureAwait(false); + + if (_hostedLifecycleServices is not null) + { + await CallLifeCycle_Started(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false); + } } else { + if (_hostedLifecycleServices is not null) + { + foreach (IHostedLifecycleService hostedService in _hostedLifecycleServices) + { + try + { + await hostedService.StartingAsync(token).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + break; + } + } + } + foreach (IHostedService hostedService in _hostedServices) { try @@ -106,11 +134,22 @@ public async Task StartAsync(CancellationToken cancellationToken = default) break; } } - } - if (_hostedLifecycleServices is not null) - { - await CallLifeCycle_Started(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false); + if (_hostedLifecycleServices is not null) + { + foreach (IHostedLifecycleService hostedService in _hostedLifecycleServices) + { + try + { + await hostedService.StartedAsync(token).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + break; + } + } + } } if (exceptions.Count > 0) @@ -151,7 +190,7 @@ private async Task TryExecuteBackgroundServiceAsync(BackgroundService background { // 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; } @@ -178,6 +217,14 @@ private async Task TryExecuteBackgroundServiceAsync(BackgroundService background } } + /// + /// Order: + /// IHostedLifecycleService.StoppingAsync + /// IHostApplicationLifetime.ApplicationStopping + /// IHostedService.Stop + /// IHostedLifecycleService.StoppedAsync + /// IHostApplicationLifetime.ApplicationStopped + /// public async Task StopAsync(CancellationToken cancellationToken = default) { _stopCalled = true; @@ -201,27 +248,70 @@ public async Task StopAsync(CancellationToken cancellationToken = default) CancellationToken token = linkedCts.Token; List exceptions = new(); - // Trigger IHostApplicationLifetime.ApplicationStopping - _applicationLifetime.StopApplication(); - - if (_hostedServices != null) // Started? + if (_hostedServices is null) + { + // Trigger IHostApplicationLifetime.ApplicationStopping even if StartAsync wasn't called. + _applicationLifetime.StopApplication(); + } + else { // Ensure hosted services are stopped in LIFO order IEnumerable reversedServices = _hostedServices.Reverse(); IEnumerable? reversedLifetimeServices = _hostedLifecycleServices?.Reverse(); - if (reversedLifetimeServices is not null) - { - await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false); - } - - // Run IHostedService.Stop either concurrently or serially. if (_options.ServicesStopConcurrently) { + if (reversedLifetimeServices is not null) + { + await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false); + } + + try + { + // Trigger IHostApplicationLifetime.ApplicationStopping. + _applicationLifetime.StopApplication(); + } + catch (Exception ex) + { + LogExceptions(); // Exceptions thrown from StoppingAsync(). + throw ex; + } + await CallLifeCycle_Stop(reversedServices, token, exceptions).ConfigureAwait(false); + + if (reversedLifetimeServices is not null) + { + await CallLifeCycle_Stopped(reversedLifetimeServices, token, exceptions).ConfigureAwait(false); + } } else { + if (reversedLifetimeServices is not null) + { + foreach (IHostedLifecycleService hostedService in reversedLifetimeServices) + { + try + { + await hostedService.StoppingAsync(token).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + } + + try + { + // Trigger IHostApplicationLifetime.ApplicationStopping. + _applicationLifetime.StopApplication(); + } + catch (Exception ex) + { + LogExceptions(); // Exceptions thrown from StoppingAsync(). + throw ex; + } + foreach (IHostedService hostedService in reversedServices) { try @@ -233,40 +323,46 @@ public async Task StopAsync(CancellationToken cancellationToken = default) exceptions.Add(ex); } } - } - if (reversedLifetimeServices is not null) - { - await CallLifeCycle_Stopped(reversedLifetimeServices, token, exceptions).ConfigureAwait(false); + if (reversedLifetimeServices is not null) + { + foreach (IHostedLifecycleService hostedService in reversedLifetimeServices) + { + try + { + await hostedService.StoppedAsync(token).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + } } } + LogExceptions(); + // Fire IHostApplicationLifetime.Stopped _applicationLifetime.NotifyStopped(); - try - { - await _hostLifetime.StopAsync(token).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - - if (exceptions.Count > 0) + void LogExceptions() { - if (exceptions.Count == 1) + if (exceptions.Count > 0) { - // Rethrow if it's a single error - Exception singleException = exceptions[0]; - _logger.StoppedWithException(singleException); - ExceptionDispatchInfo.Capture(singleException).Throw(); - } - else - { - var ex = new AggregateException("One or more hosted services failed to stop.", exceptions); - _logger.StoppedWithException(ex); - throw ex; + if (exceptions.Count == 1) + { + // Rethrow if it's a single error + Exception singleException = exceptions[0]; + _logger.StoppedWithException(singleException); + ExceptionDispatchInfo.Capture(singleException).Throw(); + } + else + { + var ex = new AggregateException("One or more hosted services failed to stop.", exceptions); + _logger.StoppedWithException(ex); + throw ex; + } } } } @@ -286,12 +382,22 @@ private static async Task CallLifeCycle_Starting(IEnumerable servic foreach (IHostedService service in services) { - Task? task = service.StartAsync(token); + Task task; + try + { + task = service.StartAsync(token); + } + catch (Exception ex) + { + exceptions.Add(ex); // Log exception from sync method. + continue; + } + if (task.IsCompleted) { if (task.Exception is not null) { - exceptions.Add(task.Exception); + exceptions.Add(task.Exception); // Log exception from async method. } } else @@ -370,12 +486,22 @@ private static async Task CallLifeCycle_Started(IEnumerable service foreach (IHostedService service in services) { - Task? task = task = service.StopAsync(token); + Task task; + try + { + task = service.StopAsync(token); + } + catch (Exception ex) + { + exceptions.Add(ex); // Log exception from sync method. + continue; + } + if (task.IsCompleted) { if (task.Exception is not null) { - exceptions.Add(task.Exception); + exceptions.Add(task.Exception); // Log exception from async method. } } else @@ -497,12 +643,22 @@ private static async Task CallLifeCycle_Stopped(IEnumerable>() - .AddHostedService>(); + .AddHostedService>() + .Configure(opts => opts.ServicesStartConcurrently = true); }); using (IHost host = hostBuilder.Build()) @@ -235,7 +236,8 @@ public async Task StartedConcurrently() { services .AddHostedService>() - .AddHostedService>(); + .AddHostedService>() + .Configure(opts => opts.ServicesStartConcurrently = true); }); using (IHost host = hostBuilder.Build()) @@ -313,7 +315,7 @@ public async Task StartedAsync(CancellationToken cancellationToken) [InlineData(false)] public async Task StartPhasesException(bool throwAfterAsyncCall) { - ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, true, true, true, false, false, false); + ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, throwOnStartup: true, throwOnShutdown: false); var hostBuilder = CreateHostBuilder(services => { services.AddHostedService((token) => impl); diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs index c85d9bc26048f..03b684df4087b 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs @@ -18,7 +18,8 @@ public async Task StoppingConcurrently() { services .AddHostedService>() - .AddHostedService>(); + .AddHostedService>() + .Configure(opts => opts.ServicesStopConcurrently = true); }); using (IHost host = hostBuilder.Build()) @@ -239,7 +240,8 @@ public async Task StoppedConcurrently() { services .AddHostedService>() - .AddHostedService>(); + .AddHostedService>() + .Configure(opts => opts.ServicesStopConcurrently = true); }); using (IHost host = hostBuilder.Build()) @@ -318,7 +320,7 @@ public async Task StoppedAsync(CancellationToken cancellationToken) [InlineData(false)] public async Task StopPhasesException(bool throwAfterAsyncCall) { - ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, false, false, false, true, true, true); + ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, throwOnStartup: false, throwOnShutdown: true); var hostBuilder = CreateHostBuilder(services => { services.AddHostedService((token) => impl); diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs index 518f8b364bb89..3737cdac69d83 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs @@ -2,6 +2,7 @@ // 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; @@ -50,6 +51,108 @@ public Task StartAsync(CancellationToken cancellationToken) 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()) + .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._startingOrder); + Assert.Equal(2, impl._startOrder); + Assert.Equal(3, impl._startedOrder); + Assert.Equal(4, impl._applicationStartedOrder); + Assert.Equal(5, impl._stoppingOrder); + Assert.Equal(6, impl._applicationStoppingOrder); + Assert.Equal(7, impl._stopOrder); + Assert.Equal(8, impl._stoppedOrder); + Assert.Equal(9, impl._applicationStoppedOrder); + } + } + + private class CallbackOrder_Impl : IHostedLifecycleService + { + 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; + + 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; + }); + } + + 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; + } + } + private class ExceptionImpl : IHostedLifecycleService { private bool _throwAfterAsyncCall; @@ -60,35 +163,23 @@ private class ExceptionImpl : IHostedLifecycleService public bool StopCalled = false; public bool StoppedCalled = false; - public bool ThrowOnStarting; - public bool ThrowOnStart; - public bool ThrowOnStarted; - public bool ThrowOnStopping; - public bool ThrowOnStop; - public bool ThrowOnStopped; + public bool ThrowOnStartup; + public bool ThrowOnShutdown; public ExceptionImpl( bool throwAfterAsyncCall, - bool throwOnStarting, - bool throwOnStart, - bool throwOnStarted, - bool throwOnStopping, - bool throwOnStop, - bool throwOnStopped) + bool throwOnStartup, + bool throwOnShutdown) { _throwAfterAsyncCall = throwAfterAsyncCall; - ThrowOnStarting = throwOnStarting; - ThrowOnStart = throwOnStart; - ThrowOnStarted = throwOnStarted; - ThrowOnStopping = throwOnStopping; - ThrowOnStop = throwOnStop; - ThrowOnStopped = throwOnStopped; + ThrowOnStartup = throwOnStartup; + ThrowOnShutdown = throwOnShutdown; } public async Task StartingAsync(CancellationToken cancellationToken) { StartingCalled = true; - if (ThrowOnStarting) + if (ThrowOnStartup) { if (_throwAfterAsyncCall) { @@ -102,7 +193,7 @@ public async Task StartingAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken) { StartCalled = true; - if (ThrowOnStart) + if (ThrowOnStartup) { if (_throwAfterAsyncCall) { @@ -116,7 +207,7 @@ public async Task StartAsync(CancellationToken cancellationToken) public async Task StartedAsync(CancellationToken cancellationToken) { StartedCalled = true; - if (ThrowOnStarted) + if (ThrowOnStartup) { if (_throwAfterAsyncCall) { @@ -130,7 +221,7 @@ public async Task StartedAsync(CancellationToken cancellationToken) public async Task StoppingAsync(CancellationToken cancellationToken) { StoppingCalled = true; - if (ThrowOnStopping) + if (ThrowOnShutdown) { if (_throwAfterAsyncCall) { @@ -144,7 +235,7 @@ public async Task StoppingAsync(CancellationToken cancellationToken) public async Task StopAsync(CancellationToken cancellationToken) { StopCalled = true; - if (ThrowOnStop) + if (ThrowOnShutdown) { if (_throwAfterAsyncCall) { @@ -158,7 +249,7 @@ public async Task StopAsync(CancellationToken cancellationToken) public async Task StoppedAsync(CancellationToken cancellationToken) { StoppedCalled = true; - if (ThrowOnStopped) + if (ThrowOnShutdown) { if (_throwAfterAsyncCall) { From 39518bcfd1ea7af1a29e49f86fb885186f6513cc Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Thu, 29 Jun 2023 13:18:28 -0500 Subject: [PATCH 3/5] Fix issue with IHostLifetime --- .../src/Internal/Host.cs | 79 +++++++++---------- .../tests/UnitTests/LifecycleTests.cs | 37 ++++++--- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index 9623fb333e04d..932d8d757df8c 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -57,6 +57,7 @@ public Host(IServiceProvider services, /// /// Order: + /// IHostLifetime.WaitForStartAsync /// IHostedLifecycleService.StartingAsync /// IHostedService.Start /// IHostedLifecycleService.StartedAsync @@ -82,13 +83,15 @@ public async Task StartAsync(CancellationToken cancellationToken = default) using (linkedCts) { CancellationToken token = linkedCts.Token; - List exceptions = new(); - _hostedServices = Services.GetRequiredService>(); - _hostedLifecycleServices = GetHostLifecycles(_hostedServices); + // This may not catch exceptions. await _hostLifetime.WaitForStartAsync(token).ConfigureAwait(false); token.ThrowIfCancellationRequested(); + List exceptions = new(); + _hostedServices = Services.GetRequiredService>(); + _hostedLifecycleServices = GetHostLifecycles(_hostedServices); + // Run IHostedService.Start either concurrently or serially. if (_options.ServicesStartConcurrently) { @@ -170,6 +173,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default) } // Fire IHostApplicationLifetime.Started + // This catches all exceptions and does not re-throw. _applicationLifetime.NotifyStarted(); } @@ -212,6 +216,8 @@ 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(); } } @@ -224,6 +230,7 @@ private async Task TryExecuteBackgroundServiceAsync(BackgroundService background /// IHostedService.Stop /// IHostedLifecycleService.StoppedAsync /// IHostApplicationLifetime.ApplicationStopped + /// IHostLifetime.StopAsync /// public async Task StopAsync(CancellationToken cancellationToken = default) { @@ -266,16 +273,9 @@ public async Task StopAsync(CancellationToken cancellationToken = default) await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false); } - try - { - // Trigger IHostApplicationLifetime.ApplicationStopping. - _applicationLifetime.StopApplication(); - } - catch (Exception ex) - { - LogExceptions(); // Exceptions thrown from StoppingAsync(). - throw ex; - } + // Trigger IHostApplicationLifetime.ApplicationStopping. + // This catches all exceptions and does not re-throw. + _applicationLifetime.StopApplication(); await CallLifeCycle_Stop(reversedServices, token, exceptions).ConfigureAwait(false); @@ -301,16 +301,9 @@ public async Task StopAsync(CancellationToken cancellationToken = default) } } - try - { - // Trigger IHostApplicationLifetime.ApplicationStopping. - _applicationLifetime.StopApplication(); - } - catch (Exception ex) - { - LogExceptions(); // Exceptions thrown from StoppingAsync(). - throw ex; - } + // Trigger IHostApplicationLifetime.ApplicationStopping. + // This catches all exceptions and does not re-throw. + _applicationLifetime.StopApplication(); foreach (IHostedService hostedService in reversedServices) { @@ -341,28 +334,34 @@ public async Task StopAsync(CancellationToken cancellationToken = default) } } - LogExceptions(); - // Fire IHostApplicationLifetime.Stopped + // This catches all exceptions and does not re-throw. _applicationLifetime.NotifyStopped(); - void LogExceptions() + // This may not catch exceptions, so we do it here. + try + { + await _hostLifetime.StopAsync(token).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + + if (exceptions.Count > 0) { - if (exceptions.Count > 0) + if (exceptions.Count == 1) { - if (exceptions.Count == 1) - { - // Rethrow if it's a single error - Exception singleException = exceptions[0]; - _logger.StoppedWithException(singleException); - ExceptionDispatchInfo.Capture(singleException).Throw(); - } - else - { - var ex = new AggregateException("One or more hosted services failed to stop.", exceptions); - _logger.StoppedWithException(ex); - throw ex; - } + // Rethrow if it's a single error + Exception singleException = exceptions[0]; + _logger.StoppedWithException(singleException); + ExceptionDispatchInfo.Capture(singleException).Throw(); + } + else + { + var ex = new AggregateException("One or more hosted services failed to stop.", exceptions); + _logger.StoppedWithException(ex); + throw ex; } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs index 3737cdac69d83..373913cc3de6b 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.cs @@ -61,6 +61,7 @@ public async Task CallbackOrder(bool concurrently) services .AddHostedService() .AddSingleton((sp) => sp.GetServices().OfType().First()) + .AddSingleton((sp) => sp.GetServices().OfType().First()) .Configure(opts => opts.ServicesStartConcurrently = concurrently) .Configure(opts => opts.ServicesStopConcurrently = concurrently); }); @@ -71,20 +72,23 @@ public async Task CallbackOrder(bool concurrently) await host.StartAsync(); await host.StopAsync(); - Assert.Equal(1, impl._startingOrder); - Assert.Equal(2, impl._startOrder); - Assert.Equal(3, impl._startedOrder); - Assert.Equal(4, impl._applicationStartedOrder); - Assert.Equal(5, impl._stoppingOrder); - Assert.Equal(6, impl._applicationStoppingOrder); - Assert.Equal(7, impl._stopOrder); - Assert.Equal(8, impl._stoppedOrder); - Assert.Equal(9, impl._applicationStoppedOrder); + 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 + private class CallbackOrder_Impl : IHostedLifecycleService, IHostLifetime { + public int _hostWaitForStartAsyncOrder; public int _startingOrder; public int _startOrder; public int _startedOrder; @@ -94,6 +98,7 @@ private class CallbackOrder_Impl : IHostedLifecycleService public int _stopOrder; public int _stoppedOrder; public int _applicationStoppedOrder; + public int _hostStoppedOrder; private int _callCount; @@ -117,6 +122,12 @@ public CallbackOrder_Impl(IServiceProvider provider) }); } + Task IHostLifetime.WaitForStartAsync(CancellationToken cancellationToken) + { + _hostWaitForStartAsyncOrder = ++_callCount; + return Task.CompletedTask; + } + public Task StartingAsync(CancellationToken cancellationToken) { _startingOrder = ++_callCount; @@ -151,6 +162,12 @@ 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 From daf2c474c5998ce8d3f9fe99b8d195d58074b0ff Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Thu, 29 Jun 2023 16:12:06 -0500 Subject: [PATCH 4/5] Share code across callbacks --- .../src/Internal/Host.cs | 497 +++--------------- .../tests/UnitTests/Internal/HostTests.cs | 4 +- 2 files changed, 86 insertions(+), 415 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index 932d8d757df8c..6c3869eef5357 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -91,68 +91,31 @@ public async Task StartAsync(CancellationToken cancellationToken = default) List exceptions = new(); _hostedServices = Services.GetRequiredService>(); _hostedLifecycleServices = GetHostLifecycles(_hostedServices); + bool concurrent = _options.ServicesStartConcurrently; + bool abortOnFirstException = !concurrent; - // Run IHostedService.Start either concurrently or serially. - if (_options.ServicesStartConcurrently) + if (_hostedLifecycleServices is not null) { - if (_hostedLifecycleServices is not null) - { - await CallLifeCycle_Starting(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false); - } - - await CallLifeCycle_Start(_hostedServices, token, exceptions).ConfigureAwait(false); - - if (_hostedLifecycleServices is not null) - { - await CallLifeCycle_Started(_hostedLifecycleServices, token, exceptions).ConfigureAwait(false); - } + // Call StartingAsync(). + await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, (service, token) => service.StartingAsync(token)).ConfigureAwait(false); } - else - { - if (_hostedLifecycleServices is not null) - { - foreach (IHostedLifecycleService hostedService in _hostedLifecycleServices) - { - try - { - await hostedService.StartingAsync(token).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); - break; - } - } - } - foreach (IHostedService hostedService in _hostedServices) + // Call StartAsync(). + await ForeachService(_hostedServices, token, concurrent, abortOnFirstException, exceptions, + async (service, token) => { - try - { - await StartAndTryToExecuteAsync(hostedService, token).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); - break; - } - } + await service.StartAsync(token).ConfigureAwait(false); - if (_hostedLifecycleServices is not null) - { - foreach (IHostedLifecycleService hostedService in _hostedLifecycleServices) + if (service is BackgroundService backgroundService) { - try - { - await hostedService.StartedAsync(token).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); - break; - } + _ = TryExecuteBackgroundServiceAsync(backgroundService); } - } + }).ConfigureAwait(false); + + if (_hostedLifecycleServices is not null) + { + // Call StartedAsync(). + await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, (service, token) => service.StartedAsync(token)).ConfigureAwait(false); } if (exceptions.Count > 0) @@ -172,7 +135,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default) } } - // Fire IHostApplicationLifetime.Started + // Call IHostApplicationLifetime.Started // This catches all exceptions and does not re-throw. _applicationLifetime.NotifyStarted(); } @@ -180,16 +143,6 @@ public async Task StartAsync(CancellationToken cancellationToken = default) _logger.Started(); } - private async Task StartAndTryToExecuteAsync(IHostedService service, CancellationToken token) - { - await service.StartAsync(token).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) @@ -253,11 +206,13 @@ public async Task StopAsync(CancellationToken cancellationToken = default) using (linkedCts) { CancellationToken token = linkedCts.Token; - List exceptions = new(); - if (_hostedServices is null) + List exceptions = new(); + if (_hostedServices is null) // Started? { - // Trigger IHostApplicationLifetime.ApplicationStopping even if StartAsync wasn't called. + + // Call IHostApplicationLifetime.ApplicationStopping. + // This catches all exceptions and does not re-throw. _applicationLifetime.StopApplication(); } else @@ -265,76 +220,29 @@ public async Task StopAsync(CancellationToken cancellationToken = default) // Ensure hosted services are stopped in LIFO order IEnumerable reversedServices = _hostedServices.Reverse(); IEnumerable? reversedLifetimeServices = _hostedLifecycleServices?.Reverse(); + bool concurrent = _options.ServicesStopConcurrently; - if (_options.ServicesStopConcurrently) + // Call StoppingAsync(). + if (reversedLifetimeServices is not null) { - if (reversedLifetimeServices is not null) - { - await CallLifeCycle_Stopping(reversedLifetimeServices, token, exceptions).ConfigureAwait(false); - } - - // Trigger IHostApplicationLifetime.ApplicationStopping. - // This catches all exceptions and does not re-throw. - _applicationLifetime.StopApplication(); - - await CallLifeCycle_Stop(reversedServices, token, exceptions).ConfigureAwait(false); - - if (reversedLifetimeServices is not null) - { - await CallLifeCycle_Stopped(reversedLifetimeServices, token, exceptions).ConfigureAwait(false); - } + await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StoppingAsync(token)).ConfigureAwait(false); } - else - { - if (reversedLifetimeServices is not null) - { - foreach (IHostedLifecycleService hostedService in reversedLifetimeServices) - { - try - { - await hostedService.StoppingAsync(token).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - } - } - // Trigger IHostApplicationLifetime.ApplicationStopping. - // This catches all exceptions and does not re-throw. - _applicationLifetime.StopApplication(); + // Call IHostApplicationLifetime.ApplicationStopping. + // This catches all exceptions and does not re-throw. + _applicationLifetime.StopApplication(); - foreach (IHostedService hostedService in reversedServices) - { - try - { - await hostedService.StopAsync(token).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - } + // Call StopAsync(). + await ForeachService(reversedServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StopAsync(token)).ConfigureAwait(false); - if (reversedLifetimeServices is not null) - { - foreach (IHostedLifecycleService hostedService in reversedLifetimeServices) - { - try - { - await hostedService.StoppedAsync(token).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - } - } + if (reversedLifetimeServices is not null) + { + // 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(); @@ -369,315 +277,78 @@ public async Task StopAsync(CancellationToken cancellationToken = default) _logger.Stopped(); } - /// - /// Call . - /// 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. - /// - private static async Task CallLifeCycle_Starting(IEnumerable services, CancellationToken token, List exceptions) + private static async Task ForeachService( + IEnumerable services, + CancellationToken token, + bool concurrent, + bool abortOnFirstException, + List exceptions, + Func operation) { - List? tasks = null; - - foreach (IHostedLifecycleService service in services) + if (concurrent) { - Task task; - try - { - task = service.StartingAsync(token); - } - catch (Exception ex) - { - exceptions.Add(ex); // Log exception from sync method. - continue; - } + // 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; - if (task.IsCompleted) + foreach (T service in services) { - if (task.Exception is not null) + Task task; + try { - exceptions.Add(task.Exception); // Log exception from async method. + task = operation(service, token); } - } - else - { - tasks ??= new(); - tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); - } - } - } - - /// - /// Call . - /// 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. - /// - private static async Task CallLifeCycle_Start(IEnumerable services, CancellationToken token, List exceptions) - { - List? tasks = null; - - foreach (IHostedService service in services) - { - Task task; - try - { - task = service.StartAsync(token); - } - catch (Exception ex) - { - exceptions.Add(ex); // Log exception from sync method. - continue; - } - - if (task.IsCompleted) - { - if (task.Exception is not null) + catch (Exception ex) { - exceptions.Add(task.Exception); // Log exception from async method. + exceptions.Add(ex); // Log exception from sync method. + continue; } - } - else - { - tasks ??= new(); - tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); - } - } - } - - /// - /// Call . - /// 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. - /// - private static async Task CallLifeCycle_Started(IEnumerable services, CancellationToken token, List exceptions) - { - List? tasks = null; - foreach (IHostedLifecycleService service in services) - { - Task task; - try - { - task = service.StartedAsync(token); - } - catch (Exception ex) - { - exceptions.Add(ex); // Log exception from sync method. - continue; - } - - if (task.IsCompleted) - { - if (task.Exception is not null) + if (task.IsCompleted) { - exceptions.Add(task.Exception); // Log exception from async method. + if (task.Exception is not null) + { + exceptions.Add(task.Exception); // Log exception from async method. + } } - } - else - { - tasks ??= new(); - tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); - } - } - } - - /// - /// Call . - /// 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. - /// - private static async Task CallLifeCycle_Stopping(IEnumerable services, CancellationToken token, List exceptions) - { - List? tasks = null; - - foreach (IHostedLifecycleService service in services) - { - Task task; - try - { - task = service.StoppingAsync(token); - } - catch (Exception ex) - { - exceptions.Add(ex); // Log exception from sync method. - continue; - } - - if (task.IsCompleted) - { - if (task.Exception is not null) + else { - exceptions.Add(task.Exception); // Log exception from async method. + tasks ??= new(); + tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token)); } } - else - { - tasks ??= new(); - tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token)); - } - } - - if (tasks is not null) - { - Task groupedTasks = Task.WhenAll(tasks); - try - { - await groupedTasks.ConfigureAwait(false); - } - catch (Exception ex) + if (tasks is not null) { - exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable()); - } - } - } - - /// - /// Call . - /// 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. - /// - private static async Task CallLifeCycle_Stop(IEnumerable services, CancellationToken token, List exceptions) - { - List? tasks = null; + Task groupedTasks = Task.WhenAll(tasks); - foreach (IHostedService service in services) - { - Task task; - try - { - task = service.StopAsync(token); - } - catch (Exception ex) - { - exceptions.Add(ex); // Log exception from sync method. - continue; - } - - if (task.IsCompleted) - { - if (task.Exception is not null) + try { - exceptions.Add(task.Exception); // Log exception from async method. + await groupedTasks.ConfigureAwait(false); } - } - else - { - tasks ??= new(); - tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), 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()); - } - } - } - - /// - /// Call . - /// 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. - /// - private static async Task CallLifeCycle_Stopped(IEnumerable services, CancellationToken token, List exceptions) - { - List? tasks = null; - - foreach (IHostedLifecycleService service in services) - { - Task task; - try - { - task = service.StoppedAsync(token); - } - catch (Exception ex) - { - exceptions.Add(ex); // Log exception from sync method. - continue; - } - - if (task.IsCompleted) - { - if (task.Exception is not null) + catch (Exception ex) { - exceptions.Add(task.Exception); // Log exception from async method. + exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable()); } } - else - { - tasks ??= new(); - tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token)); - } } - - if (tasks is not null) + else { - Task groupedTasks = Task.WhenAll(tasks); - - try - { - await groupedTasks.ConfigureAwait(false); - } - catch (Exception ex) + foreach (T service in services) { - exceptions.AddRange(groupedTasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable()); + try + { + await operation(service, token).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptions.Add(ex); + if (abortOnFirstException) + { + return; + } + } } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs index 23cf84e91621d..4f9aa27b2f439 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -237,7 +237,7 @@ public async Task AppCrashesOnStartWhenFirstHostedServiceThrows(int eventCount, }) .Build()) { - if (concurrentStartup && eventCount > 1) + if (concurrentStartup) { await Assert.ThrowsAsync(() => host.StartAsync()); } @@ -1159,7 +1159,7 @@ public async Task HostDoesNotNotifyIHostApplicationLifetimeCallbacksIfIHostedSer var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted); var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); - if (concurrentStartup && eventCount > 1) + if (concurrentStartup) { await Assert.ThrowsAsync(() => host.StartAsync()); } From cdf42fcd293023e027fcc361b0545ef1b6d0f7bc Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Fri, 30 Jun 2023 10:28:01 -0500 Subject: [PATCH 5/5] Revert prior test change; fix aggregate exception --- .../src/Internal/Host.cs | 19 ++++++++++++------- .../tests/UnitTests/Internal/HostTests.cs | 4 ++-- .../tests/UnitTests/LifecycleTests.Start.cs | 5 ----- .../tests/UnitTests/LifecycleTests.Stop.cs | 4 ---- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index 6c3869eef5357..c64f899fc4dfa 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -97,7 +97,8 @@ public async Task StartAsync(CancellationToken cancellationToken = default) if (_hostedLifecycleServices is not null) { // Call StartingAsync(). - await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, (service, token) => service.StartingAsync(token)).ConfigureAwait(false); + await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, + (service, token) => service.StartingAsync(token)).ConfigureAwait(false); } // Call StartAsync(). @@ -115,7 +116,8 @@ await ForeachService(_hostedServices, token, concurrent, abortOnFirstException, if (_hostedLifecycleServices is not null) { // Call StartedAsync(). - await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, (service, token) => service.StartedAsync(token)).ConfigureAwait(false); + await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions, + (service, token) => service.StartedAsync(token)).ConfigureAwait(false); } if (exceptions.Count > 0) @@ -225,7 +227,8 @@ public async Task StopAsync(CancellationToken cancellationToken = default) // Call StoppingAsync(). if (reversedLifetimeServices is not null) { - await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StoppingAsync(token)).ConfigureAwait(false); + await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, + (service, token) => service.StoppingAsync(token)).ConfigureAwait(false); } // Call IHostApplicationLifetime.ApplicationStopping. @@ -233,12 +236,14 @@ public async Task StopAsync(CancellationToken cancellationToken = default) _applicationLifetime.StopApplication(); // Call StopAsync(). - await ForeachService(reversedServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StopAsync(token)).ConfigureAwait(false); + await ForeachService(reversedServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => + service.StopAsync(token)).ConfigureAwait(false); if (reversedLifetimeServices is not null) { // Call StoppedAsync(). - await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => service.StoppedAsync(token)).ConfigureAwait(false); + await ForeachService(reversedLifetimeServices, token, concurrent, abortOnFirstException: false, exceptions, (service, token) => + service.StoppedAsync(token)).ConfigureAwait(false); } } @@ -309,13 +314,13 @@ private static async Task ForeachService( { if (task.Exception is not null) { - exceptions.Add(task.Exception); // Log exception from async method. + exceptions.AddRange(task.Exception.InnerExceptions); // Log exception from async method. } } else { tasks ??= new(); - tasks.Add(Task.Run(async () => await task.ConfigureAwait(false), token)); + tasks.Add(Task.Run(() => task, token)); } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs index 4f9aa27b2f439..23cf84e91621d 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -237,7 +237,7 @@ public async Task AppCrashesOnStartWhenFirstHostedServiceThrows(int eventCount, }) .Build()) { - if (concurrentStartup) + if (concurrentStartup && eventCount > 1) { await Assert.ThrowsAsync(() => host.StartAsync()); } @@ -1159,7 +1159,7 @@ public async Task HostDoesNotNotifyIHostApplicationLifetimeCallbacksIfIHostedSer var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted); var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping); - if (concurrentStartup) + if (concurrentStartup && eventCount > 1) { await Assert.ThrowsAsync(() => host.StartAsync()); } diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs index 1de8c2b9c494f..31692fe563ef2 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Start.cs @@ -117,7 +117,6 @@ public async Task StartConcurrently() // 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. @@ -329,10 +328,6 @@ public async Task StartPhasesException(bool throwAfterAsyncCall) Assert.True(impl.StartCalled); Assert.True(impl.StartedCalled); - Assert.Contains("(ThrowOnStarting)", ex.Message); - Assert.Contains("(ThrowOnStart)", ex.Message); - Assert.Contains("(ThrowOnStarted)", ex.Message); - Assert.Equal(3, ex.InnerExceptions.Count); Assert.Contains("(ThrowOnStarting)", ex.InnerExceptions[0].Message); Assert.Contains("(ThrowOnStart)", ex.InnerExceptions[1].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 index 03b684df4087b..2b413d29be3b2 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/LifecycleTests.Stop.cs @@ -340,10 +340,6 @@ public async Task StopPhasesException(bool throwAfterAsyncCall) Assert.True(impl.StopCalled); Assert.True(impl.StoppedCalled); - Assert.Contains("(ThrowOnStopping)", ex.Message); - Assert.Contains("(ThrowOnStop)", ex.Message); - Assert.Contains("(ThrowOnStopped)", ex.Message); - Assert.Equal(3, ex.InnerExceptions.Count); Assert.Contains("(ThrowOnStopping)", ex.InnerExceptions[0].Message); Assert.Contains("(ThrowOnStop)", ex.InnerExceptions[1].Message);