From b4d5f00f4de5bf129166c5a6f1c4bf142244b401 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sat, 14 Mar 2026 20:57:43 -0500 Subject: [PATCH 1/2] Fix race condition in BackgroundServiceExceptionTests The StopAsync and StopTwiceAsync tests used Task.Delay(200ms) to wait for a background service (which delays 100ms then throws) to fault. On loaded CI agents, 200ms is not always enough for the exception to propagate through the host, causing Assert.Throws to see no exception. For StopHost behavior tests: wait for ApplicationStopping, which fires after the host has captured the exception and called StopApplication(). For Ignore behavior test: use a SignalingFailureService that signals a TaskCompletionSource before throwing, so the test knows the service has faulted without relying on timing. Fixes #125550 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackgroundServiceExceptionTests.cs | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs index 05e30776023592..ba65acc3e73aef 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs @@ -78,8 +78,11 @@ public async Task BackgroundService_AsynchronousException_StopAsync_ThrowsExcept using var host = builder.Build(); await host.StartAsync(); - // Wait for the background service to fail - await Task.Delay(TimeSpan.FromMilliseconds(200)); + // Wait for the host to react to the background service failure + var lifetime = host.Services.GetRequiredService(); + var stoppingTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + lifetime.ApplicationStopping.Register(() => stoppingTcs.TrySetResult()); + await stoppingTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); await Assert.ThrowsAsync(async () => { @@ -107,8 +110,11 @@ public async Task BackgroundService_AsynchronousException_StopTwiceAsync_ThrowsE using var host = builder.Build(); await host.StartAsync(); - // Wait for the background service to fail - await Task.Delay(TimeSpan.FromMilliseconds(200)); + // Wait for the host to react to the background service failure + var lifetime = host.Services.GetRequiredService(); + var stoppingTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + lifetime.ApplicationStopping.Register(() => stoppingTcs.TrySetResult()); + await stoppingTcs.Task.WaitAsync(TimeSpan.FromSeconds(10)); await Assert.ThrowsAsync(async () => { @@ -208,6 +214,7 @@ public async Task BackgroundService_IgnoreException_DoesNotThrow() [Fact] public async Task BackgroundService_IgnoreException_StopAsync_DoesNotThrow() { + var signal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var builder = new HostBuilder() .ConfigureServices(services => { @@ -216,14 +223,15 @@ public async Task BackgroundService_IgnoreException_StopAsync_DoesNotThrow() options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore; options.ShutdownTimeout = TimeSpan.FromSeconds(1); }); - services.AddHostedService(); + services.AddSingleton(signal); + services.AddHostedService(); }); using var host = builder.Build(); await host.StartAsync(); - // Wait a bit for the background service to fail - await Task.Delay(TimeSpan.FromMilliseconds(200)); + // Wait for the background service to signal it has thrown + await signal.Task.WaitAsync(TimeSpan.FromSeconds(10)); await host.StopAsync(); } @@ -294,6 +302,23 @@ private class StopFailureService : IHostedService public Task StopAsync(CancellationToken cancellationToken) => throw new InvalidOperationException("Stop failure"); } + private class SignalingFailureService : BackgroundService + { + private readonly TaskCompletionSource _signal; + + public SignalingFailureService(TaskCompletionSource signal) + { + _signal = signal; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Delay(TimeSpan.FromMilliseconds(100)); + _signal.TrySetResult(); + throw new InvalidOperationException("Signaling asynchronous failure"); + } + } + private class SuccessfulService : BackgroundService { private readonly IHostApplicationLifetime _lifetime; From a504b8d34acc7f991f0ccca3dc889378c5bd0f5e Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sat, 14 Mar 2026 21:03:50 -0500 Subject: [PATCH 2/2] Fix comment: signal fires before throw, not after Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/UnitTests/BackgroundServiceExceptionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs index ba65acc3e73aef..e29d14bb55a3e6 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/BackgroundServiceExceptionTests.cs @@ -230,7 +230,7 @@ public async Task BackgroundService_IgnoreException_StopAsync_DoesNotThrow() using var host = builder.Build(); await host.StartAsync(); - // Wait for the background service to signal it has thrown + // Wait for the background service to reach its failure point await signal.Task.WaitAsync(TimeSpan.FromSeconds(10)); await host.StopAsync();