Don't log an error when a BackgroundService is canceled due to the host being stopped...
authorEric Erhardt <eric.erhardt@microsoft.com>
Tue, 10 Aug 2021 15:18:28 +0000 (10:18 -0500)
committerGitHub <noreply@github.com>
Tue, 10 Aug 2021 15:18:28 +0000 (10:18 -0500)
* Don't log an error when a BackgroundService is canceled due to the host being stopped.

Fix #56032

* Add volatile to stopping field.

* Convert HostTests to use a logger instead of depending on EventSource.

* Make it obvious the test is using the default worker template

src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostTests.cs
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Internal/HostTests.cs
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/TestEventListener.cs [deleted file]
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/TestLoggerProvider.cs [new file with mode: 0644]

index bcaed337120e2bedc1a134fc610c9a8c3c8d5290..4041133a0a9f253cae02bc3b088e4387229b0d5a 100644 (file)
@@ -22,6 +22,7 @@ namespace Microsoft.Extensions.Hosting.Internal
         private readonly IHostEnvironment _hostEnvironment;
         private readonly PhysicalFileProvider _defaultProvider;
         private IEnumerable<IHostedService> _hostedServices;
+        private volatile bool _stopCalled;
 
         public Host(IServiceProvider services,
                     IHostEnvironment hostEnvironment,
@@ -84,6 +85,13 @@ namespace Microsoft.Extensions.Hosting.Internal
             }
             catch (Exception ex)
             {
+                // When the host is being stopped, it cancels the background services.
+                // This isn't an error condition, so don't log it as an error.
+                if (_stopCalled && backgroundService.ExecuteTask.IsCanceled && ex is OperationCanceledException)
+                {
+                    return;
+                }
+
                 _logger.BackgroundServiceFaulted(ex);
                 if (_options.BackgroundServiceExceptionBehavior == BackgroundServiceExceptionBehavior.StopHost)
                 {
@@ -95,6 +103,7 @@ namespace Microsoft.Extensions.Hosting.Internal
 
         public async Task StopAsync(CancellationToken cancellationToken = default)
         {
+            _stopCalled = true;
             _logger.Stopping();
 
             using (var cts = new CancellationTokenSource(_options.ShutdownTimeout))
index 7fe9ceca422bb2f7d5fafbf095afa5a8ed3094d6..69f69b11dd3a457dc6b87a568e908aa109da0afe 100644 (file)
@@ -2,8 +2,10 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Diagnostics.Tracing;
 using System.IO;
 using System.Linq;
 using System.Reflection;
@@ -403,5 +405,36 @@ namespace Microsoft.Extensions.Hosting.Tests
                 }
             }
         }
+
+        private class TestEventListener : EventListener
+        {
+            private volatile bool _disposed;
+
+            private ConcurrentQueue<EventWrittenEventArgs> _events = new ConcurrentQueue<EventWrittenEventArgs>();
+
+            public IEnumerable<EventWrittenEventArgs> EventData => _events;
+
+            protected override void OnEventSourceCreated(EventSource eventSource)
+            {
+                if (eventSource.Name == "Microsoft-Extensions-Logging")
+                {
+                    EnableEvents(eventSource, EventLevel.Informational);
+                }
+            }
+
+            protected override void OnEventWritten(EventWrittenEventArgs eventData)
+            {
+                if (!_disposed)
+                {
+                    _events.Enqueue(eventData);
+                }
+            }
+
+            public override void Dispose()
+            {
+                _disposed = true;
+                base.Dispose();
+            }
+        }
     }
 }
index e98aa45666ad816ded0670e479bd8c66b42b15cc..d07c2bc57eaa719cb96482788236b1e4333eea56 100644 (file)
@@ -1325,13 +1325,13 @@ namespace Microsoft.Extensions.Hosting.Internal
             BackgroundServiceExceptionBehavior testBehavior,
             params string[] expectedExceptionMessages)
         {
-            using TestEventListener listener = new TestEventListener();
+            TestLoggerProvider logger = new TestLoggerProvider();
             var backgroundDelayTaskSource = new TaskCompletionSource<bool>();
 
             using IHost host = CreateBuilder()
                 .ConfigureLogging(logging =>
                 {
-                    logging.AddEventSourceLogger();
+                    logging.AddProvider(logger);
                 })
                 .ConfigureServices((hostContext, services) =>
                 {
@@ -1350,14 +1350,10 @@ namespace Microsoft.Extensions.Hosting.Internal
 
             while (true)
             {
-                EventWrittenEventArgs[] events =
-                    listener.EventData.Where(
-                        e => e.EventSource.Name == "Microsoft-Extensions-Logging").ToArray();
-
+                LogEvent[] events = logger.GetEvents();
                 if (expectedExceptionMessages.All(
                         expectedMessage => events.Any(
-                            e => e.Payload.OfType<string>().Any(
-                                p => p.Contains(expectedMessage)))))
+                            e => e.Message.Contains(expectedMessage))))
                 {
                     break;
                 }
@@ -1367,6 +1363,37 @@ namespace Microsoft.Extensions.Hosting.Internal
             }
         }
 
+        /// <summary>
+        /// Tests that when a BackgroundService is canceled when stopping the host,
+        /// no error is logged.
+        /// </summary>
+        [Fact]
+        public async Task HostNoErrorWhenServiceIsCanceledAsPartOfStop()
+        {
+            TestLoggerProvider logger = new TestLoggerProvider();
+
+            using IHost host = CreateBuilder()
+                .ConfigureLogging(logging =>
+                {
+                    logging.AddProvider(logger);
+                })
+                .ConfigureServices(services =>
+                {
+                    services.AddHostedService<WorkerTemplateService>();
+                })
+                .Build();
+
+            host.Start();
+            await host.StopAsync();
+
+            foreach (LogEvent logEvent in logger.GetEvents())
+            {
+                Assert.True(logEvent.LogLevel < LogLevel.Error);
+
+                Assert.NotEqual("BackgroundServiceFaulted", logEvent.EventId.Name);
+            }
+        }
+
         private IHostBuilder CreateBuilder(IConfiguration config = null)
         {
             return new HostBuilder().ConfigureHostConfiguration(builder => builder.AddConfiguration(config ?? new ConfigurationBuilder().Build()));
@@ -1513,5 +1540,27 @@ namespace Microsoft.Extensions.Hosting.Internal
                 throw new Exception("Background Exception");
             }
         }
+
+        /// <summary>
+        /// A copy of the default "Worker" template.
+        /// </summary>
+        private class WorkerTemplateService : BackgroundService
+        {
+            private readonly ILogger<WorkerTemplateService> _logger;
+
+            public WorkerTemplateService(ILogger<WorkerTemplateService> logger)
+            {
+                _logger = logger;
+            }
+
+            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+            {
+                while (!stoppingToken.IsCancellationRequested)
+                {
+                    _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
+                    await Task.Delay(1000, stoppingToken);
+                }
+            }
+        }
     }
 }
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/TestEventListener.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/TestEventListener.cs
deleted file mode 100644 (file)
index bb17dfd..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Diagnostics.Tracing;
-
-namespace Microsoft.Extensions.Hosting.Tests
-{
-    internal class TestEventListener : EventListener
-    {
-        private volatile bool _disposed;
-
-        private ConcurrentQueue<EventWrittenEventArgs> _events = new ConcurrentQueue<EventWrittenEventArgs>();
-
-        public IEnumerable<EventWrittenEventArgs> EventData => _events;
-
-        protected override void OnEventSourceCreated(EventSource eventSource)
-        {
-            if (eventSource.Name == "Microsoft-Extensions-Logging")
-            {
-                EnableEvents(eventSource, EventLevel.Informational);
-            }
-        }
-
-        protected override void OnEventWritten(EventWrittenEventArgs eventData)
-        {
-            if (!_disposed)
-            {
-                _events.Enqueue(eventData);
-            }
-        }
-
-        public override void Dispose()
-        {
-            _disposed = true;
-            base.Dispose();
-        }
-    }
-}
diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/TestLoggerProvider.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/TestLoggerProvider.cs
new file mode 100644 (file)
index 0000000..85cbdce
--- /dev/null
@@ -0,0 +1,61 @@
+// 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.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.Hosting.Tests
+{
+    internal class TestLoggerProvider : ILoggerProvider
+    {
+        private readonly TestLogger _logger = new();
+
+        /// <summary>
+        /// Provides a snapshot of the current events.
+        /// </summary>
+        public LogEvent[] GetEvents() => _logger.GetEvents();
+
+        public ILogger CreateLogger(string categoryName)
+        {
+            return _logger;
+        }
+
+        public void Dispose() { }
+
+        private class TestLogger : ILogger
+        {
+            private readonly ConcurrentQueue<LogEvent> _events = new();
+
+            internal LogEvent[] GetEvents() => _events.ToArray();
+
+            public IDisposable BeginScope<TState>(TState state) => new Scope();
+
+            public bool IsEnabled(LogLevel logLevel) => true;
+
+            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
+            {
+                _events.Enqueue(new LogEvent()
+                {
+                    LogLevel = logLevel,
+                    EventId = eventId,
+                    Message = formatter(state, exception)
+                });
+            }
+
+            private class Scope : IDisposable
+            {
+                public void Dispose()
+                {
+                }
+            }
+        }
+    }
+
+    internal class LogEvent
+    {
+        public LogLevel LogLevel { get; set; }
+        public EventId EventId { get; set; }
+        public string Message { get; set; }
+    }
+}