22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System ;
5+ using System . Collections . Concurrent ;
56using System . Collections . Generic ;
67using System . Linq ;
8+ using System . Text ;
79using System . Threading ;
810using System . Threading . Tasks ;
911using Microsoft . AspNetCore . Shared ;
@@ -17,36 +19,45 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks;
1719internal 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