Skip to content

Commit f107566

Browse files
authored
Introduce per-HealthCheckRegistration parameters (#42646)
1 parent 7ca3fda commit f107566

File tree

4 files changed

+418
-65
lines changed

4 files changed

+418
-65
lines changed

src/HealthChecks/Abstractions/src/HealthCheckRegistration.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,18 @@ public TimeSpan Timeout
158158
}
159159
}
160160

161+
/// <summary>
162+
/// Gets or sets the individual delay applied to the health check after the application starts before executing
163+
/// <see cref="IHealthCheckPublisher"/> instances. The delay is applied once at startup, and does
164+
/// not apply to subsequent iterations.
165+
/// </summary>
166+
public TimeSpan? Delay { get; set; }
167+
168+
/// <summary>
169+
/// Gets or sets the individual period used for the check.
170+
/// </summary>
171+
public TimeSpan? Period { get; set; }
172+
161173
/// <summary>
162174
/// Gets or sets the health check name.
163175
/// </summary>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
#nullable enable
2+
Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration.Delay.get -> System.TimeSpan?
3+
Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration.Delay.set -> void
4+
Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration.Period.get -> System.TimeSpan?
5+
Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckRegistration.Period.set -> void

src/HealthChecks/HealthChecks/src/HealthCheckPublisherHostedService.cs

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Collections.Generic;
67
using System.Linq;
8+
using System.Text;
79
using System.Threading;
810
using System.Threading.Tasks;
911
using Microsoft.AspNetCore.Shared;
@@ -17,36 +19,45 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks;
1719
internal sealed partial class HealthCheckPublisherHostedService : IHostedService
1820
{
1921
private readonly HealthCheckService _healthCheckService;
20-
private readonly IOptions<HealthCheckPublisherOptions> _options;
22+
private readonly IOptions<HealthCheckServiceOptions> _healthCheckServiceOptions;
23+
private readonly IOptions<HealthCheckPublisherOptions> _healthCheckPublisherOptions;
2124
private readonly ILogger _logger;
2225
private readonly IHealthCheckPublisher[] _publishers;
26+
private List<Timer>? _timers;
2327

2428
private readonly CancellationTokenSource _stopping;
25-
private Timer? _timer;
2629
private CancellationTokenSource? _runTokenSource;
2730

2831
public HealthCheckPublisherHostedService(
2932
HealthCheckService healthCheckService,
30-
IOptions<HealthCheckPublisherOptions> options,
33+
IOptions<HealthCheckServiceOptions> healthCheckServiceOptions,
34+
IOptions<HealthCheckPublisherOptions> healthCheckPublisherOptions,
3135
ILogger<HealthCheckPublisherHostedService> logger,
3236
IEnumerable<IHealthCheckPublisher> publishers)
3337
{
3438
ArgumentNullThrowHelper.ThrowIfNull(healthCheckService);
35-
ArgumentNullThrowHelper.ThrowIfNull(options);
39+
ArgumentNullThrowHelper.ThrowIfNull(healthCheckServiceOptions);
40+
ArgumentNullThrowHelper.ThrowIfNull(healthCheckPublisherOptions);
3641
ArgumentNullThrowHelper.ThrowIfNull(logger);
3742
ArgumentNullThrowHelper.ThrowIfNull(publishers);
3843

3944
_healthCheckService = healthCheckService;
40-
_options = options;
45+
_healthCheckServiceOptions = healthCheckServiceOptions;
46+
_healthCheckPublisherOptions = healthCheckPublisherOptions;
4147
_logger = logger;
4248
_publishers = publishers.ToArray();
4349

4450
_stopping = new CancellationTokenSource();
4551
}
4652

53+
private (TimeSpan Delay, TimeSpan Period) GetTimerOptions(HealthCheckRegistration registration)
54+
{
55+
return (registration?.Delay ?? _healthCheckPublisherOptions.Value.Delay, registration?.Period ?? _healthCheckPublisherOptions.Value.Period);
56+
}
57+
4758
internal bool IsStopping => _stopping.IsCancellationRequested;
4859

49-
internal bool IsTimerRunning => _timer != null;
60+
internal bool IsTimerRunning => _timers != null;
5061

5162
public Task StartAsync(CancellationToken cancellationToken = default)
5263
{
@@ -55,9 +66,9 @@ public Task StartAsync(CancellationToken cancellationToken = default)
5566
return Task.CompletedTask;
5667
}
5768

58-
// IMPORTANT - make sure this is the last thing that happens in this method. The timer can
69+
// IMPORTANT - make sure this is the last thing that happens in this method. The timers can
5970
// fire before other code runs.
60-
_timer = NonCapturingTimer.Create(Timer_Tick, null, dueTime: _options.Value.Delay, period: _options.Value.Period);
71+
_timers = CreateTimers();
6172

6273
return Task.CompletedTask;
6374
}
@@ -78,16 +89,49 @@ public Task StopAsync(CancellationToken cancellationToken = default)
7889
return Task.CompletedTask;
7990
}
8091

81-
_timer?.Dispose();
82-
_timer = null;
92+
if (_timers != null)
93+
{
94+
foreach (var timer in _timers)
95+
{
96+
timer.Dispose();
97+
}
98+
99+
_timers = null;
100+
}
83101

84102
return Task.CompletedTask;
85103
}
86104

87-
// Yes, async void. We need to be async. We need to be void. We handle the exceptions in RunAsync
88-
private async void Timer_Tick(object? state)
105+
private List<Timer> CreateTimers()
89106
{
90-
await RunAsync().ConfigureAwait(false);
107+
var delayPeriodGroups = new HashSet<(TimeSpan Delay, TimeSpan Period)>();
108+
foreach (var hc in _healthCheckServiceOptions.Value.Registrations)
109+
{
110+
var timerOptions = GetTimerOptions(hc);
111+
delayPeriodGroups.Add(timerOptions);
112+
}
113+
114+
var timers = new List<Timer>(delayPeriodGroups.Count);
115+
foreach (var group in delayPeriodGroups)
116+
{
117+
var timer = CreateTimer(group);
118+
timers.Add(timer);
119+
}
120+
121+
return timers;
122+
}
123+
124+
private Timer CreateTimer((TimeSpan Delay, TimeSpan Period) timerOptions)
125+
{
126+
return
127+
NonCapturingTimer.Create(
128+
async (state) =>
129+
{
130+
await RunAsync(timerOptions).ConfigureAwait(false);
131+
},
132+
null,
133+
dueTime: timerOptions.Delay,
134+
period: timerOptions.Period);
91135
}
92136

93137
// Internal for testing
@@ -97,21 +141,21 @@ internal void CancelToken()
97141
}
98142

99143
// Internal for testing
100-
internal async Task RunAsync()
144+
internal async Task RunAsync((TimeSpan Delay, TimeSpan Period) timerOptions)
101145
{
102146
var duration = ValueStopwatch.StartNew();
103147
Logger.HealthCheckPublisherProcessingBegin(_logger);
104148

105149
CancellationTokenSource? cancellation = null;
106150
try
107151
{
108-
var timeout = _options.Value.Timeout;
152+
var timeout = _healthCheckPublisherOptions.Value.Timeout;
109153

110154
cancellation = CancellationTokenSource.CreateLinkedTokenSource(_stopping.Token);
111155
_runTokenSource = cancellation;
112156
cancellation.CancelAfter(timeout);
113157

114-
await RunAsyncCore(cancellation.Token).ConfigureAwait(false);
158+
await RunAsyncCore(timerOptions, cancellation.Token).ConfigureAwait(false);
115159

116160
Logger.HealthCheckPublisherProcessingEnd(_logger, duration.GetElapsedTime());
117161
}
@@ -131,13 +175,21 @@ internal async Task RunAsync()
131175
}
132176
}
133177

134-
private async Task RunAsyncCore(CancellationToken cancellationToken)
178+
private async Task RunAsyncCore((TimeSpan Delay, TimeSpan Period) timerOptions, CancellationToken cancellationToken)
135179
{
136180
// Forcibly yield - we want to unblock the timer thread.
137181
await Task.Yield();
138182

183+
// Concatenate predicates - we only run HCs at the set delay and period
184+
var withOptionsPredicate = (HealthCheckRegistration r) =>
185+
{
186+
// First check whether the current timer options correspond to the current registration,
187+
// and then check the user-defined predicate if any.
188+
return (GetTimerOptions(r) == timerOptions) && (_healthCheckPublisherOptions?.Value.Predicate ?? (_ => true))(r);
189+
};
190+
139191
// The health checks service does it's own logging, and doesn't throw exceptions.
140-
var report = await _healthCheckService.CheckHealthAsync(_options.Value.Predicate, cancellationToken).ConfigureAwait(false);
192+
var report = await _healthCheckService.CheckHealthAsync(withOptionsPredicate, cancellationToken).ConfigureAwait(false);
141193

142194
var publishers = _publishers;
143195
var tasks = new Task[publishers.Length];

0 commit comments

Comments
 (0)