From f107566c2ef1c9dd648b04509249838d656ac72b Mon Sep 17 00:00:00 2001 From: Francesco Bonacci Date: Sat, 29 Jul 2023 00:49:35 +0100 Subject: [PATCH] Introduce per-HealthCheckRegistration parameters (#42646) --- .../src/HealthCheckRegistration.cs | 12 + .../Abstractions/src/PublicAPI.Unshipped.txt | 4 + .../src/HealthCheckPublisherHostedService.cs | 88 +++- .../HealthCheckPublisherHostedServiceTest.cs | 379 +++++++++++++++--- 4 files changed, 418 insertions(+), 65 deletions(-) diff --git a/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs b/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs index 34ea3ce41119..8294aec323c5 100644 --- a/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs +++ b/src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs @@ -158,6 +158,18 @@ public TimeSpan Timeout } } + /// + /// Gets or sets the individual delay applied to the health check after the application starts before executing + /// instances. The delay is applied once at startup, and does + /// not apply to subsequent iterations. + /// + public TimeSpan? Delay { get; set; } + + /// + /// Gets or sets the individual period used for the check. + /// + public TimeSpan? Period { get; set; } + /// /// Gets or sets the health check name. /// diff --git a/src/HealthChecks/Abstractions/src/PublicAPI.Unshipped.txt b/src/HealthChecks/Abstractions/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..c607cf13fa6d 100644 --- a/src/HealthChecks/Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/HealthChecks/Abstractions/src/PublicAPI.Unshipped.txt @@ -1 +1,5 @@ #nullable enable +Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration.Delay.get -> System.TimeSpan? +Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration.Delay.set -> void +Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration.Period.get -> System.TimeSpan? +Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration.Period.set -> void diff --git a/src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs b/src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs index ea0cbe5de69a..894a40b5e63c 100644 --- a/src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs +++ b/src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs @@ -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.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Shared; @@ -17,36 +19,45 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks; internal sealed partial class HealthCheckPublisherHostedService : IHostedService { private readonly HealthCheckService _healthCheckService; - private readonly IOptions _options; + private readonly IOptions _healthCheckServiceOptions; + private readonly IOptions _healthCheckPublisherOptions; private readonly ILogger _logger; private readonly IHealthCheckPublisher[] _publishers; + private List? _timers; private readonly CancellationTokenSource _stopping; - private Timer? _timer; private CancellationTokenSource? _runTokenSource; public HealthCheckPublisherHostedService( HealthCheckService healthCheckService, - IOptions options, + IOptions healthCheckServiceOptions, + IOptions healthCheckPublisherOptions, ILogger logger, IEnumerable publishers) { ArgumentNullThrowHelper.ThrowIfNull(healthCheckService); - ArgumentNullThrowHelper.ThrowIfNull(options); + ArgumentNullThrowHelper.ThrowIfNull(healthCheckServiceOptions); + ArgumentNullThrowHelper.ThrowIfNull(healthCheckPublisherOptions); ArgumentNullThrowHelper.ThrowIfNull(logger); ArgumentNullThrowHelper.ThrowIfNull(publishers); _healthCheckService = healthCheckService; - _options = options; + _healthCheckServiceOptions = healthCheckServiceOptions; + _healthCheckPublisherOptions = healthCheckPublisherOptions; _logger = logger; _publishers = publishers.ToArray(); _stopping = new CancellationTokenSource(); } + private (TimeSpan Delay, TimeSpan Period) GetTimerOptions(HealthCheckRegistration registration) + { + return (registration?.Delay ?? _healthCheckPublisherOptions.Value.Delay, registration?.Period ?? _healthCheckPublisherOptions.Value.Period); + } + internal bool IsStopping => _stopping.IsCancellationRequested; - internal bool IsTimerRunning => _timer != null; + internal bool IsTimerRunning => _timers != null; public Task StartAsync(CancellationToken cancellationToken = default) { @@ -55,9 +66,9 @@ public Task StartAsync(CancellationToken cancellationToken = default) return Task.CompletedTask; } - // IMPORTANT - make sure this is the last thing that happens in this method. The timer can + // IMPORTANT - make sure this is the last thing that happens in this method. The timers can // fire before other code runs. - _timer = NonCapturingTimer.Create(Timer_Tick, null, dueTime: _options.Value.Delay, period: _options.Value.Period); + _timers = CreateTimers(); return Task.CompletedTask; } @@ -78,16 +89,49 @@ public Task StopAsync(CancellationToken cancellationToken = default) return Task.CompletedTask; } - _timer?.Dispose(); - _timer = null; + if (_timers != null) + { + foreach (var timer in _timers) + { + timer.Dispose(); + } + + _timers = null; + } return Task.CompletedTask; } - // Yes, async void. We need to be async. We need to be void. We handle the exceptions in RunAsync - private async void Timer_Tick(object? state) + private List CreateTimers() { - await RunAsync().ConfigureAwait(false); + var delayPeriodGroups = new HashSet<(TimeSpan Delay, TimeSpan Period)>(); + foreach (var hc in _healthCheckServiceOptions.Value.Registrations) + { + var timerOptions = GetTimerOptions(hc); + delayPeriodGroups.Add(timerOptions); + } + + var timers = new List(delayPeriodGroups.Count); + foreach (var group in delayPeriodGroups) + { + var timer = CreateTimer(group); + timers.Add(timer); + } + + return timers; + } + + private Timer CreateTimer((TimeSpan Delay, TimeSpan Period) timerOptions) + { + return + NonCapturingTimer.Create( + async (state) => + { + await RunAsync(timerOptions).ConfigureAwait(false); + }, + null, + dueTime: timerOptions.Delay, + period: timerOptions.Period); } // Internal for testing @@ -97,7 +141,7 @@ internal void CancelToken() } // Internal for testing - internal async Task RunAsync() + internal async Task RunAsync((TimeSpan Delay, TimeSpan Period) timerOptions) { var duration = ValueStopwatch.StartNew(); Logger.HealthCheckPublisherProcessingBegin(_logger); @@ -105,13 +149,13 @@ internal async Task RunAsync() CancellationTokenSource? cancellation = null; try { - var timeout = _options.Value.Timeout; + var timeout = _healthCheckPublisherOptions.Value.Timeout; cancellation = CancellationTokenSource.CreateLinkedTokenSource(_stopping.Token); _runTokenSource = cancellation; cancellation.CancelAfter(timeout); - await RunAsyncCore(cancellation.Token).ConfigureAwait(false); + await RunAsyncCore(timerOptions, cancellation.Token).ConfigureAwait(false); Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime()); } @@ -131,13 +175,21 @@ internal async Task RunAsync() } } - private async Task RunAsyncCore(CancellationToken cancellationToken) + private async Task RunAsyncCore((TimeSpan Delay, TimeSpan Period) timerOptions, CancellationToken cancellationToken) { // Forcibly yield - we want to unblock the timer thread. await Task.Yield(); + // Concatenate predicates - we only run HCs at the set delay and period + var withOptionsPredicate = (HealthCheckRegistration r) => + { + // First check whether the current timer options correspond to the current registration, + // and then check the user-defined predicate if any. + return (GetTimerOptions(r) == timerOptions) && (_healthCheckPublisherOptions?.Value.Predicate ?? (_ => true))(r); + }; + // The health checks service does it's own logging, and doesn't throw exceptions. - var report = await _healthCheckService.CheckHealthAsync(_options.Value.Predicate, cancellationToken).ConfigureAwait(false); + var report = await _healthCheckService.CheckHealthAsync(withOptionsPredicate, cancellationToken).ConfigureAwait(false); var publishers = _publishers; var tasks = new Task[publishers.Length]; diff --git a/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs b/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs index b7dcd8e6d42f..2e6d01658f24 100644 --- a/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs +++ b/src/HealthChecks/HealthChecks/test/HealthCheckPublisherHostedServiceTest.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; using Xunit; #nullable enable @@ -106,7 +107,7 @@ public async Task StartAsync_WithPublishers_StartsTimer_RunsPublishers() new TestPublisher() { Wait = unblock2.Task, }, }; - var service = CreateService(publishers, configure: (options) => + var service = CreateService(publishers, configurePublisherOptions: (options) => { options.Delay = TimeSpan.FromMilliseconds(0); }); @@ -142,29 +143,26 @@ public async Task StopAsync_CancelsExecution() // Arrange var unblock = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var publishers = new TestPublisher[] - { - new TestPublisher() { Wait = unblock.Task, } - }; + var publisher = new TestPublisher() { Wait = unblock.Task, }; - var service = CreateService(publishers); + var service = CreateService(new[] { publisher }); try { await service.StartAsync(); // Start execution - var running = service.RunAsync(); + var running = RunServiceAsync(service); // Wait for the publisher to see the cancellation token - await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); - Assert.Single(publishers[0].Entries); + await publisher.Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + Assert.Single(publisher.Entries); // Act await service.StopAsync(); // Trigger cancellation // Assert - await AssertCanceledAsync(publishers[0].Entries[0].cancellationToken); + await AssertCanceledAsync(publisher.Entries[0].cancellationToken); Assert.False(service.IsTimerRunning); Assert.True(service.IsStopping); @@ -188,21 +186,18 @@ public async Task RunAsync_WaitsForCompletion_Single() var unblock = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var publishers = new TestPublisher[] - { - new TestPublisher() { Wait = unblock.Task, }, - }; + var publisher = new TestPublisher() { Wait = unblock.Task, }; - var service = CreateService(publishers, sink: sink); + var service = CreateService(new[] { publisher }, sink: sink); try { await service.StartAsync(); // Act - var running = service.RunAsync(); + var running = RunServiceAsync(service); - await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + await publisher.Started.TimeoutAfter(TimeSpan.FromSeconds(10)); unblock.SetResult(null); @@ -212,11 +207,8 @@ public async Task RunAsync_WaitsForCompletion_Single() Assert.True(service.IsTimerRunning); Assert.False(service.IsStopping); - for (var i = 0; i < publishers.Length; i++) - { - var report = Assert.Single(publishers[i].Entries).report; - Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k)); - } + var report = Assert.Single(publisher.Entries).report; + Assert.Equal(new[] { "one", "two", }, report.Entries.Keys.OrderBy(k => k)); } finally { @@ -239,6 +231,164 @@ public async Task RunAsync_WaitsForCompletion_Single() entry => { Assert.Equal(HealthCheckPublisherEventIds.HealthCheckPublisherProcessingEnd, entry.EventId); }); } + [Fact] + public async Task RunAsync_WaitsForCompletion_Single_RegistrationParameters() + { + // Arrange + const string HealthyMessage = "Everything is A-OK"; + + var unblock = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var unblockDelayedCheck = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var publisher = new TestPublisher() { Wait = unblock.Task, }; + + var service = CreateService(new[] { publisher }, configureBuilder: b => + { + b.Add( + new HealthCheckRegistration( + name: "CheckDefault", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null)); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay1Period9", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(1), + Period = TimeSpan.FromSeconds(9) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay1Period9_1", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(1), + Period = TimeSpan.FromSeconds(9) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay1Period18", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(1), + Period = TimeSpan.FromSeconds(18) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay2Period18", + instance: new DelegateHealthCheck(_ => + { + unblockDelayedCheck.TrySetResult(null); // Unblock 2s delayed check + return Task.FromResult(HealthCheckResult.Healthy(HealthyMessage)); + }), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(2), + Period = TimeSpan.FromSeconds(18) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay7Period11", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(7), + Period = TimeSpan.FromSeconds(11) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay9Period5", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(9), + Period = TimeSpan.FromSeconds(5) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay10Period8", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(10), + Period = TimeSpan.FromSeconds(8) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay10Period9", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(10), + Period = TimeSpan.FromSeconds(9) + }); + }); + + try + { + var running = RunServiceAsync(service); + + await publisher.Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + + await Task.Yield(); + Assert.False(running.IsCompleted); + + unblock.SetResult(null); + + await running.TimeoutAfter(TimeSpan.FromSeconds(10)); + + // The timer hasn't started yet. Only the default 5 minute registration is run by RunServiceAsync. + Assert.Equal("CheckDefault", Assert.Single(Assert.Single(publisher.Entries).report.Entries.Keys)); + + await service.StartAsync(); + await unblockDelayedCheck.Task.TimeoutAfter(TimeSpan.FromSeconds(10)); + + Assert.True(service.IsTimerRunning); + Assert.False(service.IsStopping); + } + finally + { + await service.StopAsync(); + Assert.False(service.IsTimerRunning); + Assert.True(service.IsStopping); + } + + // Assert - after stop + var entries = publisher.Entries.SelectMany(e => e.report.Entries.Select(e2 => e2.Key)).OrderBy(k => k).ToArray(); + Assert.Contains("CheckDefault", entries); + Assert.Contains("CheckDelay1Period18", entries); + Assert.Contains("CheckDelay1Period9", entries); + Assert.Contains("CheckDelay1Period9_1", entries); + } + // Not testing logs here to avoid differences in logging order [Fact] public async Task RunAsync_WaitsForCompletion_Multiple() @@ -262,7 +412,7 @@ public async Task RunAsync_WaitsForCompletion_Multiple() await service.StartAsync(); // Act - var running = service.RunAsync(); + var running = RunServiceAsync(service); await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); await publishers[1].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); @@ -299,25 +449,22 @@ public async Task RunAsync_PublishersCanTimeout() var sink = new TestSink(); var unblock = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var publishers = new TestPublisher[] - { - new TestPublisher() { Wait = unblock.Task, }, - }; + var publisher = new TestPublisher() { Wait = unblock.Task, }; - var service = CreateService(publishers, sink: sink); + var service = CreateService(new[] { publisher }, sink: sink); try { await service.StartAsync(); // Act - var running = service.RunAsync(); + var running = RunServiceAsync(service); - await publishers[0].Started.TimeoutAfter(TimeSpan.FromSeconds(10)); + await publisher.Started.TimeoutAfter(TimeSpan.FromSeconds(10)); service.CancelToken(); - await AssertCanceledAsync(publishers[0].Entries[0].cancellationToken); + await AssertCanceledAsync(publisher.Entries[0].cancellationToken); unblock.SetResult(null); @@ -352,29 +499,147 @@ public async Task RunAsync_PublishersCanTimeout() public async Task RunAsync_CanFilterHealthChecks() { // Arrange + const string HealthyMessage = "Everything is A-OK"; + + var unblockDelayedCheck = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var publishers = new TestPublisher[] { new TestPublisher(), new TestPublisher(), }; - var service = CreateService(publishers, configure: (options) => - { - options.Predicate = (r) => r.Name == "one"; - }); + var service = CreateService( + publishers, + configurePublisherOptions: (options) => + { + options.Predicate = (r) => r.Name.Contains("Delay") && !r.Name.Contains("_2"); + options.Delay = TimeSpan.Zero; + }, + configureBuilder: b => + { + b.Add( + new HealthCheckRegistration( + name: "CheckDefault", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null)); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay1Period9", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(1), + Period = TimeSpan.FromSeconds(9) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay1Period9_1", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(1), + Period = TimeSpan.FromSeconds(9) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay1Period9_2", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(1), + Period = TimeSpan.FromSeconds(9) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay2Period18", + instance: new DelegateHealthCheck(_ => + { + unblockDelayedCheck.TrySetResult(null); // Unblock 2s delayed check + return Task.FromResult(HealthCheckResult.Healthy(HealthyMessage)); + }), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(2), + Period = TimeSpan.FromSeconds(18) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay7Period11", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(7), + Period = TimeSpan.FromSeconds(11) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay9Period5", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(9), + Period = TimeSpan.FromSeconds(5) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay10Period8", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(10), + Period = TimeSpan.FromSeconds(8) + }); + + b.Add( + new HealthCheckRegistration( + name: "CheckDelay10Period9", + instance: new DelegateHealthCheck(_ => Task.FromResult(HealthCheckResult.Healthy(HealthyMessage))), + failureStatus: null, + tags: null, + timeout: default) + { + Delay = TimeSpan.FromSeconds(10), + Period = TimeSpan.FromSeconds(9) + }); + }); try { await service.StartAsync(); // Act - await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10)); + await unblockDelayedCheck.Task.TimeoutAfter(TimeSpan.FromSeconds(10)); // Assert for (var i = 0; i < publishers.Length; i++) { - var report = Assert.Single(publishers[i].Entries).report; - Assert.Equal(new[] { "one", }, report.Entries.Keys.OrderBy(k => k)); + var entries = publishers[i].Entries.SelectMany(e => e.report.Entries.Select(e2 => e2.Key)).OrderBy(k => k).ToArray(); + + Assert.Contains("CheckDelay1Period9", entries); + Assert.Contains("CheckDelay1Period9_1", entries); } } finally @@ -383,6 +648,15 @@ public async Task RunAsync_CanFilterHealthChecks() Assert.False(service.IsTimerRunning); Assert.True(service.IsStopping); } + + // Assert - after stop + for (var i = 0; i < publishers.Length; i++) + { + var entries = publishers[i].Entries.SelectMany(e => e.report.Entries.Select(e2 => e2.Key)).OrderBy(k => k).ToArray(); + + Assert.Contains("CheckDelay1Period9", entries); + Assert.Contains("CheckDelay1Period9_1", entries); + } } [Fact] @@ -402,7 +676,7 @@ public async Task RunAsync_HandlesExceptions() await service.StartAsync(); // Act - await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10)); + await RunServiceAsync(service).TimeoutAfter(TimeSpan.FromSeconds(10)); } finally @@ -446,7 +720,7 @@ public async Task RunAsync_HandlesExceptions_Multiple() await service.StartAsync(); // Act - await service.RunAsync().TimeoutAfter(TimeSpan.FromSeconds(10)); + await RunServiceAsync(service).TimeoutAfter(TimeSpan.FromSeconds(10)); } finally @@ -459,15 +733,24 @@ public async Task RunAsync_HandlesExceptions_Multiple() private HealthCheckPublisherHostedService CreateService( IHealthCheckPublisher[] publishers, - Action? configure = null, + Action? configurePublisherOptions = null, + Action? configureBuilder = null, TestSink? sink = null) { var serviceCollection = new ServiceCollection(); serviceCollection.AddOptions(); serviceCollection.AddLogging(); - serviceCollection.AddHealthChecks() - .AddCheck("one", () => { return HealthCheckResult.Healthy(); }) - .AddCheck("two", () => { return HealthCheckResult.Healthy(); }); + + IHealthChecksBuilder builder = serviceCollection.AddHealthChecks(); + if (configureBuilder == null) + { + builder.AddCheck("one", () => { return HealthCheckResult.Healthy(); }) + .AddCheck("two", () => { return HealthCheckResult.Healthy(); }); + } + else + { + configureBuilder(builder); + } // Choosing big values for tests to make sure that we're not dependent on the defaults. // All of the tests that rely on the timer will set their own values for speed. @@ -476,7 +759,7 @@ private HealthCheckPublisherHostedService CreateService( options.Delay = TimeSpan.FromMinutes(5); options.Period = TimeSpan.FromMinutes(5); options.Timeout = TimeSpan.FromMinutes(5); - }); + }); if (publishers != null) { @@ -486,9 +769,9 @@ private HealthCheckPublisherHostedService CreateService( } } - if (configure != null) + if (configurePublisherOptions != null) { - serviceCollection.Configure(configure); + serviceCollection.Configure(configurePublisherOptions); } if (sink != null) @@ -500,6 +783,8 @@ private HealthCheckPublisherHostedService CreateService( return services.GetServices().OfType().Single(); } + private Task RunServiceAsync(HealthCheckPublisherHostedService service) => service.RunAsync((TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5))); + private static async Task AssertCanceledAsync(CancellationToken cancellationToken) { await Assert.ThrowsAsync(() => Task.Delay(TimeSpan.FromSeconds(10), cancellationToken));