Propagate BackgroundService exceptions from the host#124863
Propagate BackgroundService exceptions from the host#124863svick wants to merge 2 commits intodotnet:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request fixes issue #67146 by ensuring that exceptions thrown asynchronously by BackgroundServices propagate to the caller and result in a non-zero exit code, matching the behavior when exceptions are thrown synchronously.
Changes:
- Added exception tracking and propagation for background services in StopHost behavior mode
- Replaced trivial enum tests with comprehensive functional tests covering various exception scenarios
- Added logging for exception propagation
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs | Added _backgroundServiceExceptions list to track background service exceptions and propagate them at the end of StopAsync(), ensuring non-zero exit codes |
| src/libraries/Microsoft.Extensions.Hosting/src/Internal/HostingLoggerExtensions.cs | Added BackgroundServiceExceptionsPropagating logging method and fixed signature of StoppedWithException to require non-null exception |
| src/libraries/Microsoft.Extensions.Hosting/src/Internal/LoggerEventIds.cs | Added BackgroundServiceExceptionsPropagating event ID |
| src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs | Added comprehensive tests for synchronous/asynchronous exceptions, multiple exceptions, and different exception behaviors |
| src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionBehaviorTests.cs | Removed trivial enum value tests in favor of functional tests |
Comments suppressed due to low confidence (6)
src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs:146
- The comment change from "Call" to "Cancel" is inaccurate. While
NotifyStarted()internally cancels a CancellationTokenSource to signal the ApplicationStarted event, the comment should describe the semantic action (calling/triggering the notification), not the internal implementation detail. The original wording "Call IHostApplicationLifetime.Started" better describes what the code is doing at this level of abstraction.
// Cancel IHostApplicationLifetime.Started
src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs:239
- The comment change from "Call" to "Cancel" is inaccurate. While
StopApplication()internally cancels a CancellationTokenSource to signal the ApplicationStopping event, the comment should describe the semantic action (calling/triggering the event), not the internal implementation detail. The original wording "Call IHostApplicationLifetime.ApplicationStopping" better describes what the code is doing at this level of abstraction.
// Cancel IHostApplicationLifetime.ApplicationStopping.
src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs:259
- The comment change from "Call" to "Cancel" is inaccurate. While
StopApplication()internally cancels a CancellationTokenSource to signal the ApplicationStopping event, the comment should describe the semantic action (calling/triggering the event), not the internal implementation detail. The original wording "Call IHostApplicationLifetime.ApplicationStopping" better describes what the code is doing at this level of abstraction.
// Cancel IHostApplicationLifetime.ApplicationStopping.
src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs:275
- The comment change from "Call" to "Cancel" is inaccurate. While
NotifyStopped()internally cancels a CancellationTokenSource to signal the ApplicationStopped event, the comment should describe the semantic action (calling/triggering the notification), not the internal implementation detail. The original wording "Call IHostApplicationLifetime.Stopped" better describes what the code is doing at this level of abstraction.
// Cancel IHostApplicationLifetime.Stopped
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs:82
- The hardcoded 500ms delay creates a race condition and could lead to flaky tests. Consider using ApplicationStopping.WaitHandle.WaitOne (as seen in HostTests.cs line 710) or TaskCompletionSource patterns (as seen in HostTests.cs lines 713-758) for more reliable synchronization. These approaches ensure the background service has actually failed before calling StopAsync, rather than relying on timing assumptions.
await Task.Delay(TimeSpan.FromMilliseconds(500));
src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs:163
- The hardcoded 2-second delay is longer than necessary and slows down test execution. Consider using ApplicationStopping.WaitHandle.WaitOne or TaskCompletionSource patterns for more efficient and reliable synchronization, as demonstrated in existing tests in HostTests.cs.
await Task.Delay(TimeSpan.FromSeconds(2));
Fixes #67146 by throwing from
IHost.RunAsync/StopAsyncwhen aBackgroundServicefails with an exception andBackgroundServiceExceptionBehavior.StopHostis set.I think this should be documented as a breaking change, since users could be relying on having a throwing
BackgroundServicethat stops the application but returns with success exit code.