From 3173bd7f418cf125e29ad6bc2a634936ddafaf6b Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 31 Mar 2021 11:27:29 -0500 Subject: [PATCH 01/17] Added background service exception behavior enum. Update HostOptions and Host accordingly. --- ...crosoft.Extensions.Hosting.Abstractions.cs | 5 ++++ .../src/BackgroundService.cs | 1 - .../src/BackgroundServiceExceptionBehavior.cs | 29 +++++++++++++++++++ .../ref/Microsoft.Extensions.Hosting.cs | 1 + .../src/HostOptions.cs | 6 ++++ .../src/Internal/Host.cs | 5 ++-- 6 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundServiceExceptionBehavior.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 8a9cb2f15adea9..2afb78ff2f0f13 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 @@ -23,6 +23,11 @@ public virtual void Dispose() { } public virtual System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public virtual System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken) { throw null; } } + public enum BackgroundServiceExceptionBehavior + { + Ignore, + StopHost + } [System.ObsoleteAttribute("This type is obsolete and will be removed in a future version. The recommended alternative is Microsoft.Extensions.Hosting.Environments.", false)] public static partial class EnvironmentName { diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs index 5e736c11fb8a78..cb2ace1ecea9d8 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs @@ -75,7 +75,6 @@ public virtual async Task StopAsync(CancellationToken cancellationToken) // Wait until the task completes or the stop token triggers await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false); } - } public virtual void Dispose() diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundServiceExceptionBehavior.cs b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundServiceExceptionBehavior.cs new file mode 100644 index 00000000000000..1406fb265c1d94 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundServiceExceptionBehavior.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Hosting +{ + /// + /// Specifies a behavior that the will honor if an + /// unhandled exception occurs in one of its instances. + /// + public enum BackgroundServiceExceptionBehavior + { + /// + /// Ignore exceptions thrown in . + /// + /// + /// If a throws an exception, the will ignore it. + /// The is not restarted. + /// + Ignore = 0, + + /// + /// Stops the instance, and terminates the process. + /// + /// + /// If a throws an exception, the will terminate the process. + /// + StopHost = 1 + } +} 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 1f92e812f2fc8b..af5087dfc7d4d1 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs @@ -56,6 +56,7 @@ public partial class HostOptions { public HostOptions() { } public System.TimeSpan ShutdownTimeout { get { throw null; } set { } } + public Microsoft.Extensions.Hosting.BackgroundServiceExceptionBehavior BackgroundServiceExceptionBehavior { 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 3b825718642d0f..befbf4da5eb930 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs @@ -17,6 +17,12 @@ public class HostOptions /// public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5); + /// + /// The behavior the will follow when any of + /// its instances throw an unhandled exception. + /// + public BackgroundServiceExceptionBehavior BackgroundServiceExceptionBehavior { get; set; } + internal void Initialize(IConfiguration configuration) { var timeoutSeconds = configuration["shutdownTimeoutSeconds"]; diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index ca78d42a05bc6f..4bc8f03ab56f47 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -66,7 +66,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default) if (hostedService is BackgroundService backgroundService) { - _ = HandleBackgroundException(backgroundService); + _ = HandleBackgroundExceptionAsync(backgroundService); } } @@ -76,13 +76,14 @@ public async Task StartAsync(CancellationToken cancellationToken = default) _logger.Started(); } - private async Task HandleBackgroundException(BackgroundService backgroundService) + private async Task HandleBackgroundExceptionAsync(BackgroundService backgroundService) { try { await backgroundService.ExecuteTask.ConfigureAwait(false); } catch (Exception ex) + when (_options.BackgroundServiceExceptionBehavior is BackgroundServiceExceptionBehavior.Ignore) { _logger.BackgroundServiceFaulted(ex); } From 91a31ec34774882de370b429819a32826210753b Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 1 Apr 2021 10:08:31 -0500 Subject: [PATCH 02/17] Added corresponding unit tests for host when ignoring or stopping. --- .../src/Internal/Host.cs | 22 +++-- .../src/Resources/Strings.resx | 49 +++++----- .../tests/UnitTests/HostTests.cs | 2 +- .../tests/UnitTests/Internal/HostTests.cs | 93 ++++++++++++++++++- ...osoft.Extensions.Hosting.Unit.Tests.csproj | 1 + 5 files changed, 136 insertions(+), 31 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index 4bc8f03ab56f47..ead7af198f3564 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -61,12 +61,15 @@ public async Task StartAsync(CancellationToken cancellationToken = default) foreach (IHostedService hostedService in _hostedServices) { - // Fire IHostedService.Start - await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false); - if (hostedService is BackgroundService backgroundService) { - _ = HandleBackgroundExceptionAsync(backgroundService); + _ = TryStartBackgroundServiceAsync( + backgroundService, combinedCancellationToken); + } + else + { + // Fire IHostedService.Start + await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false); } } @@ -76,16 +79,23 @@ public async Task StartAsync(CancellationToken cancellationToken = default) _logger.Started(); } - private async Task HandleBackgroundExceptionAsync(BackgroundService backgroundService) + private async Task TryStartBackgroundServiceAsync( + BackgroundService backgroundService, CancellationToken cancellationToken) { try { + // Fire IHostedService.Start + await backgroundService.StartAsync(cancellationToken).ConfigureAwait(false); await backgroundService.ExecuteTask.ConfigureAwait(false); } catch (Exception ex) - when (_options.BackgroundServiceExceptionBehavior is BackgroundServiceExceptionBehavior.Ignore) { _logger.BackgroundServiceFaulted(ex); + if (_options is { BackgroundServiceExceptionBehavior: BackgroundServiceExceptionBehavior.StopHost }) + { + Environment.FailFast( + SR.Format(SR.BackgroundServiceExceptionStoppedHost, ex)); + } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx index c4de18cb0e1e17..fdea133d073451 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx @@ -2,16 +2,16 @@ @@ -117,7 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost. A BackgroundService has thrown an unhandled exception, and the IHost instance is stopping. To avoid this behavior, configure this to Ignore; however the BackgroundService will not be restarted. Error: {0} @@ -134,4 +134,4 @@ Error: {0} The resolver returned a null IServiceProviderFactory - \ No newline at end of file + From 0819a2d7a33c8330d0520ba4b01c20d0dea234cf Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 1 Apr 2021 16:21:31 -0500 Subject: [PATCH 05/17] Remove the RemoteExecutor, as it is no longer needed. --- .../tests/UnitTests/HostBuilderTests.cs | 19 +++ .../tests/UnitTests/Internal/HostTests.cs | 120 +++++------------- ...osoft.Extensions.Hosting.Unit.Tests.csproj | 1 - 3 files changed, 49 insertions(+), 91 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs index df2d42a574c4bb..6702861ea5ae24 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs @@ -572,6 +572,25 @@ public void HostBuilderConfigureDefaultsInterleavesMissingConfigValues() Assert.Equal(expectedContentRootPath, env.ContentRootPath); } + [Fact] + public void HostBuilderCanConfigureBackgroundServiceExceptionBehavior() + { + using IHost host = new HostBuilder() + .ConfigureServices( + services => + services.Configure( + options => + options.BackgroundServiceExceptionBehavior = + BackgroundServiceExceptionBehavior.StopHost)) + .Build(); + + var options = host.Services.GetRequiredService>(); + + Assert.Equal( + BackgroundServiceExceptionBehavior.StopHost, + options.Value.BackgroundServiceExceptionBehavior); + } + private class FakeFileProvider : IFileProvider, IDisposable { public bool Disposed { get; private set; } 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 57f0a06f7059fb..8f1ef506b6af6d 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting.Fakes; @@ -659,84 +658,34 @@ public async Task WebHostStopAsyncUsesDefaultTimeoutIfNoTokenProvided() } [Fact] - public void HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBehaviorOfStopHost() + public async Task HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBehaviorOfStopHost() { - RemoteExecutor.Invoke(async () => - { - using IHost host = CreateBuilder() - .ConfigureServices( - services => - { - services.AddHostedService(); - services.Configure( - options => - options.BackgroundServiceExceptionBehavior = - BackgroundServiceExceptionBehavior.StopHost); - }) - .Build(); - - await host.StartAsync(); - - var lifetime = host.Services.GetRequiredService(); - var wasStoppingCalled = false; - lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true); - var wasStoppedCalled = false; - lifetime.ApplicationStopped.Register(() => wasStoppedCalled = true); - - Assert.True( - wasStoppingCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, gracefully stopping the host."); - Assert.True( - wasStoppedCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, gracefully stopping the host."); - }).Dispose(); - } - - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBehaviorOfStopHostAfterSometime() - { - RemoteExecutor.Invoke(async () => - { - var backgroundDelayTaskSource = new TaskCompletionSource(); - - using IHost host = CreateBuilder() - .ConfigureServices( - services => - { - services.AddHostedService( - _ => new AsyncThrowingService(backgroundDelayTaskSource.Task)); - - services.Configure( - options => - options.BackgroundServiceExceptionBehavior = - BackgroundServiceExceptionBehavior.StopHost); - }) - .Build(); + using IHost host = CreateBuilder() + .ConfigureServices( + services => + { + services.AddHostedService(); + services.Configure( + options => + options.BackgroundServiceExceptionBehavior = + BackgroundServiceExceptionBehavior.StopHost); + }) + .Build(); - var lifetime = host.Services.GetRequiredService(); - var wasStoppingCalled = false; - lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true); - var wasStoppedCalled = false; - lifetime.ApplicationStopped.Register(() => wasStoppedCalled = true); + await host.StartAsync(); - await Task.WhenAll(host.StartAsync(), DelayThenSignalContinueAsync()); + var lifetime = host.Services.GetRequiredService(); + var wasStoppingCalled = false; + lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true); + var wasStoppedCalled = false; + lifetime.ApplicationStopped.Register(() => wasStoppedCalled = true); - async Task DelayThenSignalContinueAsync() - { - // Emulate the background service doing some work successfully - // for 5 seconds before throwing exception. - await Task.Delay(5000); - - backgroundDelayTaskSource.SetResult(true); - }; - - Assert.True( - wasStoppingCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, gracefully stopping the host."); - Assert.True( - wasStoppedCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, gracefully stopping the host."); - }).Dispose(); + Assert.True( + wasStoppingCalled, + $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, gracefully stopping the host."); + Assert.True( + wasStoppedCalled, + $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, gracefully stopping the host."); } [Fact] @@ -761,15 +710,12 @@ public void HostHandlesExceptionsThrownWithBackgroundServiceExceptionBehaviorOfI var lifetime = host.Services.GetRequiredService(); var wasStoppingCalled = false; lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true); - var wasStoppedCalled = false; - lifetime.ApplicationStopped.Register(() => wasStoppedCalled = true); host.Start(); backgroundDelayTaskSource.SetResult(true); Assert.False(wasStoppingCalled); - Assert.False(wasStoppedCalled); } [Fact] @@ -1490,8 +1436,6 @@ private class AsyncThrowingService : BackgroundService { private readonly Task? _executeDelayTask; - internal bool Disposed { get; private set; } - public AsyncThrowingService() { } public AsyncThrowingService(Task executeDelayTask) @@ -1501,17 +1445,13 @@ public AsyncThrowingService(Task executeDelayTask) protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - if (_executeDelayTask is { }) - await _executeDelayTask; - - throw new Exception("Background Exception"); - } - - public override void Dispose() - { - Disposed = true; + while (true) + { + if (_executeDelayTask is { }) + await _executeDelayTask; - base.Dispose(); + throw new Exception("Background Exception"); + } } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj index c5049661e96a16..8038a16a4d5afd 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj @@ -3,7 +3,6 @@ $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent);net461 true - true From e985ec5dbf26f269c33c39fea3b016764280077b Mon Sep 17 00:00:00 2001 From: David Pine Date: Mon, 5 Apr 2021 12:54:43 -0500 Subject: [PATCH 06/17] Call `IApplicationLifetime.StopApplication()` instead of `StopAsync()`. --- .../src/Internal/Host.cs | 3 +-- .../tests/UnitTests/Internal/HostTests.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index c2647ace55b5fb..a1083d6016ce60 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -94,8 +94,7 @@ private async Task TryStartBackgroundServiceAsync( if (_options is { BackgroundServiceExceptionBehavior: BackgroundServiceExceptionBehavior.StopHost }) { _logger.LogCritical(SR.Format(SR.BackgroundServiceExceptionStoppedHost, ex)); - - await StopAsync(CancellationToken.None).ConfigureAwait(false); + _applicationLifetime.StopApplication(); } } } 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 8f1ef506b6af6d..14899cded411a2 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -672,20 +672,20 @@ public async Task HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBe }) .Build(); - await host.StartAsync(); - var lifetime = host.Services.GetRequiredService(); + var wasStartedCalled = false; + lifetime.ApplicationStarted.Register(() => wasStartedCalled = true); var wasStoppingCalled = false; lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true); - var wasStoppedCalled = false; - lifetime.ApplicationStopped.Register(() => wasStoppedCalled = true); + + await host.StartAsync(); Assert.True( - wasStoppingCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, gracefully stopping the host."); + wasStartedCalled, + $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, started should not have been called."); Assert.True( - wasStoppedCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, gracefully stopping the host."); + wasStoppingCalled, + $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, stopping should have been called."); } [Fact] From 28ccffc848a877b181fc0f4938865329a2964533 Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 7 Apr 2021 13:24:12 -0500 Subject: [PATCH 07/17] Moved enum out of abstractions. Encapsulate logging logic in extensions. Define new EventId. Simplify check, avoid null eval. Update triple dash comments per peer review. --- .../src/BackgroundServiceExceptionBehavior.cs | 6 +++--- .../src/Internal/Host.cs | 4 ++-- .../src/Internal/HostingLoggerExtensions.cs | 17 ++++++++++++++--- .../src/Internal/LoggerEventIds.cs | 19 ++++++++++--------- 4 files changed, 29 insertions(+), 17 deletions(-) rename src/libraries/{Microsoft.Extensions.Hosting.Abstractions => Microsoft.Extensions.Hosting}/src/BackgroundServiceExceptionBehavior.cs (82%) diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundServiceExceptionBehavior.cs b/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs similarity index 82% rename from src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundServiceExceptionBehavior.cs rename to src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs index 1406fb265c1d94..b763d17b245ba5 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundServiceExceptionBehavior.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs @@ -13,16 +13,16 @@ public enum BackgroundServiceExceptionBehavior /// Ignore exceptions thrown in . /// /// - /// If a throws an exception, the will ignore it. + /// If a throws an exception, the will log the error, but otherwise ignore it. /// The is not restarted. /// Ignore = 0, /// - /// Stops the instance, and terminates the process. + /// Stops the instance. /// /// - /// If a throws an exception, the will terminate the process. + /// If a throws an exception, the instance stops, but the process continues. /// StopHost = 1 } diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index a1083d6016ce60..e5643e99a23a70 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -91,9 +91,9 @@ private async Task TryStartBackgroundServiceAsync( catch (Exception ex) { _logger.BackgroundServiceFaulted(ex); - if (_options is { BackgroundServiceExceptionBehavior: BackgroundServiceExceptionBehavior.StopHost }) + if (_options.BackgroundServiceExceptionBehavior == BackgroundServiceExceptionBehavior.StopHost) { - _logger.LogCritical(SR.Format(SR.BackgroundServiceExceptionStoppedHost, ex)); + _logger.BackgroundServiceStoppingHost(ex); _applicationLifetime.StopApplication(); } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs index a3c722f9653369..0ed5b337937d93 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs @@ -11,12 +11,11 @@ internal static class HostingLoggerExtensions { public static void ApplicationError(this ILogger logger, EventId eventId, string message, Exception exception) { - var reflectionTypeLoadException = exception as ReflectionTypeLoadException; - if (reflectionTypeLoadException != null) + if (exception is ReflectionTypeLoadException reflectionTypeLoadException) { foreach (Exception ex in reflectionTypeLoadException.LoaderExceptions) { - message = message + Environment.NewLine + ex.Message; + message = $"{message}{Environment.NewLine}{ex.Message}"; } } @@ -87,5 +86,17 @@ public static void BackgroundServiceFaulted(this ILogger logger, Exception ex) message: "BackgroundService failed"); } } + + public static void BackgroundServiceStoppingHost(this ILogger logger, Exception ex) + { + if (logger.IsEnabled(LogLevel.Critical)) + { + logger.LogCritical( + eventId: LoggerEventIds.BackgroundServiceStoppingHost, + exception: ex, + message: SR.BackgroundServiceExceptionStoppedHost, + args: ex); + } + } } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs index 666266c1d50d43..9271953313a05c 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs @@ -7,14 +7,15 @@ namespace Microsoft.Extensions.Hosting.Internal { internal static class LoggerEventIds { - public static readonly EventId Starting = new EventId(1, "Starting"); - public static readonly EventId Started = new EventId(2, "Started"); - public static readonly EventId Stopping = new EventId(3, "Stopping"); - public static readonly EventId Stopped = new EventId(4, "Stopped"); - public static readonly EventId StoppedWithException = new EventId(5, "StoppedWithException"); - public static readonly EventId ApplicationStartupException = new EventId(6, "ApplicationStartupException"); - public static readonly EventId ApplicationStoppingException = new EventId(7, "ApplicationStoppingException"); - public static readonly EventId ApplicationStoppedException = new EventId(8, "ApplicationStoppedException"); - public static readonly EventId BackgroundServiceFaulted = new EventId(9, "BackgroundServiceFaulted"); + public static readonly EventId Starting = new EventId(1, nameof(Starting)); + public static readonly EventId Started = new EventId(2, nameof(Started)); + public static readonly EventId Stopping = new EventId(3, nameof(Stopping)); + public static readonly EventId Stopped = new EventId(4, nameof(Stopped)); + public static readonly EventId StoppedWithException = new EventId(5, nameof(StoppedWithException)); + public static readonly EventId ApplicationStartupException = new EventId(6, nameof(ApplicationStartupException)); + public static readonly EventId ApplicationStoppingException = new EventId(7, nameof(ApplicationStoppingException)); + public static readonly EventId ApplicationStoppedException = new EventId(8, nameof(ApplicationStoppedException)); + public static readonly EventId BackgroundServiceFaulted = new EventId(9, nameof(BackgroundServiceFaulted)); + public static readonly EventId BackgroundServiceStoppingHost = new EventId(10, nameof(BackgroundServiceStoppingHost)); } } From c9356b85a441913fc9c89b1b056a0a2c72d9ceea Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 7 Apr 2021 13:44:58 -0500 Subject: [PATCH 08/17] Set the default as `StopHost` --- .../src/BackgroundServiceExceptionBehavior.cs | 2 +- .../Microsoft.Extensions.Hosting/src/HostOptions.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs b/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs index b763d17b245ba5..de5234b67efa0c 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs @@ -22,7 +22,7 @@ public enum BackgroundServiceExceptionBehavior /// Stops the instance. /// /// - /// If a throws an exception, the instance stops, but the process continues. + /// If a throws an exception, the instance stops, and the process continues. /// StopHost = 1 } diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs index befbf4da5eb930..208d945678db7b 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostOptions.cs @@ -21,7 +21,11 @@ public class HostOptions /// The behavior the will follow when any of /// its instances throw an unhandled exception. /// - public BackgroundServiceExceptionBehavior BackgroundServiceExceptionBehavior { get; set; } + /// + /// Defaults to . + /// + public BackgroundServiceExceptionBehavior BackgroundServiceExceptionBehavior { get; set; } = + BackgroundServiceExceptionBehavior.StopHost; internal void Initialize(IConfiguration configuration) { From 4632446908f663890e3a6d06675e23cc27d446d4 Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 7 Apr 2021 15:14:19 -0500 Subject: [PATCH 09/17] Move ref bits from hosting.abstractions to hosting proper. --- .../ref/Microsoft.Extensions.Hosting.Abstractions.cs | 6 +----- .../ref/Microsoft.Extensions.Hosting.cs | 5 +++++ 2 files changed, 6 insertions(+), 5 deletions(-) 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 2afb78ff2f0f13..5353d8f3acac1e 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 @@ -23,11 +23,7 @@ public virtual void Dispose() { } public virtual System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public virtual System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken) { throw null; } } - public enum BackgroundServiceExceptionBehavior - { - Ignore, - StopHost - } + [System.ObsoleteAttribute("This type is obsolete and will be removed in a future version. The recommended alternative is Microsoft.Extensions.Hosting.Environments.", false)] public static partial class EnvironmentName { 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 af5087dfc7d4d1..eed198edb6c3df 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs @@ -13,6 +13,11 @@ public static partial class OptionsBuilderExtensions } namespace Microsoft.Extensions.Hosting { + public enum BackgroundServiceExceptionBehavior + { + Ignore, + StopHost + } public partial class ConsoleLifetimeOptions { public ConsoleLifetimeOptions() { } From a74d694fcdf4a68d967962a4a49cdd8a7be0da45 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 8 Apr 2021 16:52:44 -0500 Subject: [PATCH 10/17] Address feedback from peer review --- .../Microsoft.Extensions.Hosting.Abstractions.cs | 1 - .../src/BackgroundService.cs | 1 + .../ref/Microsoft.Extensions.Hosting.cs | 4 ++-- .../src/BackgroundServiceExceptionBehavior.cs | 14 +++++++------- .../src/Internal/HostingLoggerExtensions.cs | 2 +- .../src/Resources/Strings.resx | 4 +--- .../tests/UnitTests/Internal/HostTests.cs | 14 ++++---------- 7 files changed, 16 insertions(+), 24 deletions(-) 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 5353d8f3acac1e..8a9cb2f15adea9 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 @@ -23,7 +23,6 @@ public virtual void Dispose() { } public virtual System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public virtual System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken) { throw null; } } - [System.ObsoleteAttribute("This type is obsolete and will be removed in a future version. The recommended alternative is Microsoft.Extensions.Hosting.Environments.", false)] public static partial class EnvironmentName { diff --git a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs index cb2ace1ecea9d8..5e736c11fb8a78 100644 --- a/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs +++ b/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/BackgroundService.cs @@ -75,6 +75,7 @@ public virtual async Task StopAsync(CancellationToken cancellationToken) // Wait until the task completes or the stop token triggers await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false); } + } public virtual void Dispose() 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 eed198edb6c3df..a2b2b83ae5d565 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/ref/Microsoft.Extensions.Hosting.cs @@ -15,8 +15,8 @@ namespace Microsoft.Extensions.Hosting { public enum BackgroundServiceExceptionBehavior { - Ignore, - StopHost + StopHost, + Ignore } public partial class ConsoleLifetimeOptions { diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs b/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs index de5234b67efa0c..7b3b66fc1bf3ff 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/BackgroundServiceExceptionBehavior.cs @@ -10,20 +10,20 @@ namespace Microsoft.Extensions.Hosting public enum BackgroundServiceExceptionBehavior { /// - /// Ignore exceptions thrown in . + /// Stops the instance. /// /// - /// If a throws an exception, the will log the error, but otherwise ignore it. - /// The is not restarted. + /// If a throws an exception, the instance stops, and the process continues. /// - Ignore = 0, + StopHost = 0, /// - /// Stops the instance. + /// Ignore exceptions thrown in . /// /// - /// If a throws an exception, the instance stops, and the process continues. + /// If a throws an exception, the will log the error, but otherwise ignore it. + /// The is not restarted. /// - StopHost = 1 + Ignore = 1 } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs index 0ed5b337937d93..6920079054436a 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs @@ -15,7 +15,7 @@ public static void ApplicationError(this ILogger logger, EventId eventId, string { foreach (Exception ex in reflectionTypeLoadException.LoaderExceptions) { - message = $"{message}{Environment.NewLine}{ex.Message}"; + message = message + Environment.NewLine + ex.Message; } } diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx index 9e613d4172d273..90d8aad521a279 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Resources/Strings.resx @@ -118,9 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost. A BackgroundService has thrown an unhandled exception, and the IHost instance is stopping. To avoid this behavior, configure this to Ignore; however the BackgroundService will not be restarted. - -Error: {0} + The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost. A BackgroundService has thrown an unhandled exception, and the IHost instance is stopping. To avoid this behavior, configure this to Ignore; however the BackgroundService will not be restarted. Build can only be called once. 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 14899cded411a2..8cd7e6133be7ac 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -664,7 +664,7 @@ public async Task HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBe .ConfigureServices( services => { - services.AddHostedService(); + services.AddHostedService(_ => new AsyncThrowingService(Task.CompletedTask)); services.Configure( options => options.BackgroundServiceExceptionBehavior = @@ -1434,9 +1434,7 @@ public ValueTask DisposeAsync() private class AsyncThrowingService : BackgroundService { - private readonly Task? _executeDelayTask; - - public AsyncThrowingService() { } + private readonly Task _executeDelayTask; public AsyncThrowingService(Task executeDelayTask) { @@ -1445,13 +1443,9 @@ public AsyncThrowingService(Task executeDelayTask) protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - while (true) - { - if (_executeDelayTask is { }) - await _executeDelayTask; + await _executeDelayTask; - throw new Exception("Background Exception"); - } + throw new Exception("Background Exception"); } } } From 4737d983b2d81f46e4d4a387514539b34669f9e7 Mon Sep 17 00:00:00 2001 From: David Pine Date: Mon, 12 Apr 2021 14:49:33 -0500 Subject: [PATCH 11/17] Added unit test to verify background service 1 gets started, and runs a bit - and other service can cause stop --- .../tests/UnitTests/Internal/HostTests.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) 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 8cd7e6133be7ac..017fe697c4a4ac 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -688,6 +688,41 @@ public async Task HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBe $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, stopping should have been called."); } + [Fact] + public async Task HostStopsApplicationWithOneBackgroundServiceErrorAndOthersWithoutError() + { + var executionCount = 0; + using IHost host = CreateBuilder() + .ConfigureServices( + services => + { + services.AddHostedService(_ => new TestBackgroundService(Task.Delay(200), i => executionCount = i)); + services.AddHostedService(_ => new AsyncThrowingService(Task.CompletedTask)); + services.Configure( + options => + options.BackgroundServiceExceptionBehavior = + BackgroundServiceExceptionBehavior.StopHost); + }) + .Build(); + + var lifetime = host.Services.GetRequiredService(); + var wasStartedCalled = false; + lifetime.ApplicationStarted.Register(() => wasStartedCalled = true); + var wasStoppingCalled = false; + lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true); + + await host.StartAsync(); + + Assert.True( + wasStartedCalled, + $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, started should not have been called."); + Assert.True( + wasStoppingCalled, + $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, stopping should have been called."); + + Assert.True(executionCount > 0); + } + [Fact] public void HostHandlesExceptionsThrownWithBackgroundServiceExceptionBehaviorOfIgnore() { @@ -1432,6 +1467,29 @@ public ValueTask DisposeAsync() } } + private class TestBackgroundService : BackgroundService + { + private readonly Action _counter; + private readonly Task _emulateWorkTask; + + private int _executionCount; + + public TestBackgroundService(Task emulateWorkTask, Action counter) + { + _emulateWorkTask = emulateWorkTask; + _counter = counter; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + _counter(++_executionCount); + await _emulateWorkTask; + } + } + } + private class AsyncThrowingService : BackgroundService { private readonly Task _executeDelayTask; From 5cd17054b234e2703066ed6f128d9e502668dd7a Mon Sep 17 00:00:00 2001 From: David Pine Date: Tue, 13 Apr 2021 10:14:34 -0500 Subject: [PATCH 12/17] Use explicitly controlled timing for unit test and remove args: ex --- .../src/Internal/HostingLoggerExtensions.cs | 3 +-- .../tests/UnitTests/Internal/HostTests.cs | 5 ++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs index 6920079054436a..32e560f1f74fd8 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs @@ -94,8 +94,7 @@ public static void BackgroundServiceStoppingHost(this ILogger logger, Exception logger.LogCritical( eventId: LoggerEventIds.BackgroundServiceStoppingHost, exception: ex, - message: SR.BackgroundServiceExceptionStoppedHost, - args: ex); + message: SR.BackgroundServiceExceptionStoppedHost); } } } 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 017fe697c4a4ac..ba8540016b7606 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -692,11 +692,13 @@ public async Task HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBe public async Task HostStopsApplicationWithOneBackgroundServiceErrorAndOthersWithoutError() { var executionCount = 0; + TaskCompletionSource tcs = new(); + using IHost host = CreateBuilder() .ConfigureServices( services => { - services.AddHostedService(_ => new TestBackgroundService(Task.Delay(200), i => executionCount = i)); + services.AddHostedService(_ => new TestBackgroundService(tcs.Task, i => executionCount = i)); services.AddHostedService(_ => new AsyncThrowingService(Task.CompletedTask)); services.Configure( options => @@ -712,6 +714,7 @@ public async Task HostStopsApplicationWithOneBackgroundServiceErrorAndOthersWith lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true); await host.StartAsync(); + tcs.TrySetResult(true); Assert.True( wasStartedCalled, From bad7af5bd9d68510dcba96e2ab2809c8b75ea34e Mon Sep 17 00:00:00 2001 From: David Pine Date: Tue, 13 Apr 2021 10:21:50 -0500 Subject: [PATCH 13/17] Convert from Fact, to Theory. Test both enum values. --- .../tests/UnitTests/HostBuilderTests.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs index 6702861ea5ae24..1e4721987d678b 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs @@ -572,22 +572,24 @@ public void HostBuilderConfigureDefaultsInterleavesMissingConfigValues() Assert.Equal(expectedContentRootPath, env.ContentRootPath); } - [Fact] - public void HostBuilderCanConfigureBackgroundServiceExceptionBehavior() + [Theory] + [InlineData(BackgroundServiceExceptionBehavior.Ignore)] + [InlineData(BackgroundServiceExceptionBehavior.StopHost)] + public void HostBuilderCanConfigureBackgroundServiceExceptionBehavior( + BackgroundServiceExceptionBehavior testBehavior) { using IHost host = new HostBuilder() .ConfigureServices( services => services.Configure( options => - options.BackgroundServiceExceptionBehavior = - BackgroundServiceExceptionBehavior.StopHost)) + options.BackgroundServiceExceptionBehavior = testBehavior)) .Build(); var options = host.Services.GetRequiredService>(); Assert.Equal( - BackgroundServiceExceptionBehavior.StopHost, + testBehavior, options.Value.BackgroundServiceExceptionBehavior); } From aeb6a56c1b7a8c24470263a8f538585852337785 Mon Sep 17 00:00:00 2001 From: David Pine Date: Tue, 13 Apr 2021 12:23:48 -0500 Subject: [PATCH 14/17] Adjusted test case, per @eerhardt --- .../tests/UnitTests/Internal/HostTests.cs | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) 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 ba8540016b7606..42cebac24baa9c 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -691,15 +691,15 @@ public async Task HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBe [Fact] public async Task HostStopsApplicationWithOneBackgroundServiceErrorAndOthersWithoutError() { - var executionCount = 0; + var wasOtherServiceStarted = false; TaskCompletionSource tcs = new(); using IHost host = CreateBuilder() .ConfigureServices( services => { - services.AddHostedService(_ => new TestBackgroundService(tcs.Task, i => executionCount = i)); services.AddHostedService(_ => new AsyncThrowingService(Task.CompletedTask)); + services.AddHostedService(_ => new TestBackgroundService(tcs.Task, () => wasOtherServiceStarted = true)); services.Configure( options => options.BackgroundServiceExceptionBehavior = @@ -722,8 +722,9 @@ public async Task HostStopsApplicationWithOneBackgroundServiceErrorAndOthersWith Assert.True( wasStoppingCalled, $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, stopping should have been called."); - - Assert.True(executionCount > 0); + Assert.True( + wasOtherServiceStarted, + $"The {nameof(TestBackgroundService)} should have started."); } [Fact] @@ -1470,27 +1471,24 @@ public ValueTask DisposeAsync() } } - private class TestBackgroundService : BackgroundService + private class TestBackgroundService : IHostedService { - private readonly Action _counter; + private readonly Action _onStart; private readonly Task _emulateWorkTask; - private int _executionCount; - - public TestBackgroundService(Task emulateWorkTask, Action counter) + public TestBackgroundService(Task emulateWorkTask, Action onStart) { _emulateWorkTask = emulateWorkTask; - _counter = counter; + _onStart = onStart; } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + public async Task StartAsync(CancellationToken stoppingToken) { - while (!stoppingToken.IsCancellationRequested) - { - _counter(++_executionCount); - await _emulateWorkTask; - } + _onStart(); + await _emulateWorkTask; } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } private class AsyncThrowingService : BackgroundService From d9c642d25a5dc8c852044bd537efc840ecae2d82 Mon Sep 17 00:00:00 2001 From: David Pine Date: Wed, 14 Apr 2021 09:29:53 -0500 Subject: [PATCH 15/17] Fix test hang issue --- .../tests/UnitTests/Internal/HostTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 42cebac24baa9c..a5538989577015 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -711,10 +711,13 @@ public async Task HostStopsApplicationWithOneBackgroundServiceErrorAndOthersWith var wasStartedCalled = false; lifetime.ApplicationStarted.Register(() => wasStartedCalled = true); var wasStoppingCalled = false; - lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true); + lifetime.ApplicationStopping.Register(() => + { + tcs.SetResult(true); + wasStoppingCalled = true; + }); await host.StartAsync(); - tcs.TrySetResult(true); Assert.True( wasStartedCalled, From 71c1f26f31cc8057535e509e7cb3c3b7d8bbe77c Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 15 Apr 2021 15:04:05 -0500 Subject: [PATCH 16/17] Separate StartAsync and ExecuteTask, let StartAsync bubble. Adjust unit tests... --- .../src/Internal/Host.cs | 16 ++----- .../tests/UnitTests/Internal/HostTests.cs | 47 ++++++++----------- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs index e5643e99a23a70..bcaed337120e2b 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs @@ -61,15 +61,12 @@ public async Task StartAsync(CancellationToken cancellationToken = default) foreach (IHostedService hostedService in _hostedServices) { + // Fire IHostedService.Start + await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false); + if (hostedService is BackgroundService backgroundService) { - _ = TryStartBackgroundServiceAsync( - backgroundService, combinedCancellationToken); - } - else - { - // Fire IHostedService.Start - await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false); + _ = TryExecuteBackgroundServiceAsync(backgroundService); } } @@ -79,13 +76,10 @@ public async Task StartAsync(CancellationToken cancellationToken = default) _logger.Started(); } - private async Task TryStartBackgroundServiceAsync( - BackgroundService backgroundService, CancellationToken cancellationToken) + private async Task TryExecuteBackgroundServiceAsync(BackgroundService backgroundService) { try { - // Fire IHostedService.Start - await backgroundService.StartAsync(cancellationToken).ConfigureAwait(false); await backgroundService.ExecuteTask.ConfigureAwait(false); } catch (Exception ex) 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 a5538989577015..de10a58376cd78 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -672,34 +672,29 @@ public async Task HostPropagatesExceptionsThrownWithBackgroundServiceExceptionBe }) .Build(); - var lifetime = host.Services.GetRequiredService(); - var wasStartedCalled = false; - lifetime.ApplicationStarted.Register(() => wasStartedCalled = true); - var wasStoppingCalled = false; - lifetime.ApplicationStopping.Register(() => wasStoppingCalled = true); - - await host.StartAsync(); - - Assert.True( - wasStartedCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, started should not have been called."); - Assert.True( - wasStoppingCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, stopping should have been called."); + await Assert.ThrowsAsync(() => host.StartAsync()); } [Fact] public async Task HostStopsApplicationWithOneBackgroundServiceErrorAndOthersWithoutError() - { + { var wasOtherServiceStarted = false; - TaskCompletionSource tcs = new(); + + TaskCompletionSource throwingTcs = new(); + TaskCompletionSource otherTcs = new(); using IHost host = CreateBuilder() .ConfigureServices( services => { - services.AddHostedService(_ => new AsyncThrowingService(Task.CompletedTask)); - services.AddHostedService(_ => new TestBackgroundService(tcs.Task, () => wasOtherServiceStarted = true)); + services.AddHostedService(_ => new AsyncThrowingService(throwingTcs.Task)); + services.AddHostedService( + _ => new TestBackgroundService(otherTcs.Task, + () => + { + wasOtherServiceStarted = true; + throwingTcs.SetResult(true); + })); services.Configure( options => options.BackgroundServiceExceptionBehavior = @@ -708,26 +703,22 @@ public async Task HostStopsApplicationWithOneBackgroundServiceErrorAndOthersWith .Build(); var lifetime = host.Services.GetRequiredService(); + var wasStartedCalled = false; lifetime.ApplicationStarted.Register(() => wasStartedCalled = true); + var wasStoppingCalled = false; lifetime.ApplicationStopping.Register(() => { - tcs.SetResult(true); + otherTcs.SetResult(true); wasStoppingCalled = true; }); await host.StartAsync(); - Assert.True( - wasStartedCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, started should not have been called."); - Assert.True( - wasStoppingCalled, - $"The {nameof(AsyncThrowingService)} should have thrown, calling Host.StopAsync, stopping should have been called."); - Assert.True( - wasOtherServiceStarted, - $"The {nameof(TestBackgroundService)} should have started."); + Assert.True(wasStartedCalled); + Assert.True(wasStoppingCalled); + Assert.True(wasOtherServiceStarted); } [Fact] From 3d4c11cfe6639e10ccdc6f7fded0f4bb3b1c931d Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 15 Apr 2021 20:31:32 -0500 Subject: [PATCH 17/17] Update the background service async exceptions get logged unit test --- .../tests/UnitTests/Internal/HostTests.cs | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) 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 de10a58376cd78..4138e7df4c5c08 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs @@ -1317,8 +1317,12 @@ public void ThrowExceptionForCustomImplementationOfIHostApplicationLifetime() /// Tests when a BackgroundService throws an exception asynchronously /// (after an await), the exception gets logged correctly. /// - [Fact] - public async Task BackgroundServiceAsyncExceptionGetsLogged() + [Theory] + [InlineData(BackgroundServiceExceptionBehavior.Ignore, "BackgroundService failed")] + [InlineData(BackgroundServiceExceptionBehavior.StopHost, "BackgroundService failed", "The HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost")] + public async Task BackgroundServiceAsyncExceptionGetsLogged( + BackgroundServiceExceptionBehavior testBehavior, + params string[] expectedExceptionMessages) { using TestEventListener listener = new TestEventListener(); var backgroundDelayTaskSource = new TaskCompletionSource(); @@ -1330,6 +1334,9 @@ public async Task BackgroundServiceAsyncExceptionGetsLogged() }) .ConfigureServices((hostContext, services) => { + services.Configure( + options => + options.BackgroundServiceExceptionBehavior = testBehavior); services.AddHostedService(sp => new AsyncThrowingService(backgroundDelayTaskSource.Task)); }) .Start(); @@ -1337,15 +1344,19 @@ public async Task BackgroundServiceAsyncExceptionGetsLogged() backgroundDelayTaskSource.SetResult(true); // give the background service 1 minute to log the failure - TimeSpan timeout = TimeSpan.FromMinutes(1); + var timeout = TimeSpan.FromMinutes(1); Stopwatch sw = Stopwatch.StartNew(); while (true) { - EventWrittenEventArgs[] events = listener.EventData.ToArray(); - if (events.Any(e => - e.EventSource.Name == "Microsoft-Extensions-Logging" && - e.Payload.OfType().Any(p => p.Contains("BackgroundService failed")))) + EventWrittenEventArgs[] events = + listener.EventData.Where( + e => e.EventSource.Name == "Microsoft-Extensions-Logging").ToArray(); + + if (expectedExceptionMessages.All( + expectedMessage => events.Any( + e => e.Payload.OfType().Any( + p => p.Contains(expectedMessage))))) { break; }