2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
4
using System ;
5
+ using System . Collections . Concurrent ;
5
6
using System . Collections . Generic ;
6
7
using System . Linq ;
8
+ using System . Text ;
7
9
using System . Threading ;
8
10
using System . Threading . Tasks ;
9
11
using Microsoft . AspNetCore . Shared ;
@@ -17,36 +19,45 @@ namespace Microsoft.Extensions.Diagnostics.HealthChecks;
17
19
internal sealed partial class HealthCheckPublisherHostedService : IHostedService
18
20
{
19
21
private readonly HealthCheckService _healthCheckService ;
20
- private readonly IOptions < HealthCheckPublisherOptions > _options ;
22
+ private readonly IOptions < HealthCheckServiceOptions > _healthCheckServiceOptions ;
23
+ private readonly IOptions < HealthCheckPublisherOptions > _healthCheckPublisherOptions ;
21
24
private readonly ILogger _logger ;
22
25
private readonly IHealthCheckPublisher [ ] _publishers ;
26
+ private List < Timer > ? _timers ;
23
27
24
28
private readonly CancellationTokenSource _stopping ;
25
- private Timer ? _timer ;
26
29
private CancellationTokenSource ? _runTokenSource ;
27
30
28
31
public HealthCheckPublisherHostedService (
29
32
HealthCheckService healthCheckService ,
30
- IOptions < HealthCheckPublisherOptions > options ,
33
+ IOptions < HealthCheckServiceOptions > healthCheckServiceOptions ,
34
+ IOptions < HealthCheckPublisherOptions > healthCheckPublisherOptions ,
31
35
ILogger < HealthCheckPublisherHostedService > logger ,
32
36
IEnumerable < IHealthCheckPublisher > publishers )
33
37
{
34
38
ArgumentNullThrowHelper . ThrowIfNull ( healthCheckService ) ;
35
- ArgumentNullThrowHelper . ThrowIfNull ( options ) ;
39
+ ArgumentNullThrowHelper . ThrowIfNull ( healthCheckServiceOptions ) ;
40
+ ArgumentNullThrowHelper . ThrowIfNull ( healthCheckPublisherOptions ) ;
36
41
ArgumentNullThrowHelper . ThrowIfNull ( logger ) ;
37
42
ArgumentNullThrowHelper . ThrowIfNull ( publishers ) ;
38
43
39
44
_healthCheckService = healthCheckService ;
40
- _options = options ;
45
+ _healthCheckServiceOptions = healthCheckServiceOptions ;
46
+ _healthCheckPublisherOptions = healthCheckPublisherOptions ;
41
47
_logger = logger ;
42
48
_publishers = publishers . ToArray ( ) ;
43
49
44
50
_stopping = new CancellationTokenSource ( ) ;
45
51
}
46
52
53
+ private ( TimeSpan Delay , TimeSpan Period ) GetTimerOptions ( HealthCheckRegistration registration )
54
+ {
55
+ return ( registration ? . Delay ?? _healthCheckPublisherOptions . Value . Delay , registration ? . Period ?? _healthCheckPublisherOptions . Value . Period ) ;
56
+ }
57
+
47
58
internal bool IsStopping => _stopping . IsCancellationRequested ;
48
59
49
- internal bool IsTimerRunning => _timer != null ;
60
+ internal bool IsTimerRunning => _timers != null ;
50
61
51
62
public Task StartAsync ( CancellationToken cancellationToken = default )
52
63
{
@@ -55,9 +66,9 @@ public Task StartAsync(CancellationToken cancellationToken = default)
55
66
return Task . CompletedTask ;
56
67
}
57
68
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
59
70
// fire before other code runs.
60
- _timer = NonCapturingTimer . Create ( Timer_Tick , null , dueTime : _options . Value . Delay , period : _options . Value . Period ) ;
71
+ _timers = CreateTimers ( ) ;
61
72
62
73
return Task . CompletedTask ;
63
74
}
@@ -78,16 +89,49 @@ public Task StopAsync(CancellationToken cancellationToken = default)
78
89
return Task . CompletedTask ;
79
90
}
80
91
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
+ }
83
101
84
102
return Task . CompletedTask ;
85
103
}
86
104
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 ( )
89
106
{
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 ) ;
91
135
}
92
136
93
137
// Internal for testing
@@ -97,21 +141,21 @@ internal void CancelToken()
97
141
}
98
142
99
143
// Internal for testing
100
- internal async Task RunAsync ( )
144
+ internal async Task RunAsync ( ( TimeSpan Delay , TimeSpan Period ) timerOptions )
101
145
{
102
146
var duration = ValueStopwatch . StartNew ( ) ;
103
147
Logger . HealthCheckPublisherProcessingBegin ( _logger ) ;
104
148
105
149
CancellationTokenSource ? cancellation = null ;
106
150
try
107
151
{
108
- var timeout = _options . Value . Timeout ;
152
+ var timeout = _healthCheckPublisherOptions . Value . Timeout ;
109
153
110
154
cancellation = CancellationTokenSource . CreateLinkedTokenSource ( _stopping . Token ) ;
111
155
_runTokenSource = cancellation ;
112
156
cancellation . CancelAfter ( timeout ) ;
113
157
114
- await RunAsyncCore ( cancellation . Token ) . ConfigureAwait ( false ) ;
158
+ await RunAsyncCore ( timerOptions , cancellation . Token ) . ConfigureAwait ( false ) ;
115
159
116
160
Logger . HealthCheckPublisherProcessingEnd ( _logger , duration . GetElapsedTime ( ) ) ;
117
161
}
@@ -131,13 +175,21 @@ internal async Task RunAsync()
131
175
}
132
176
}
133
177
134
- private async Task RunAsyncCore ( CancellationToken cancellationToken )
178
+ private async Task RunAsyncCore ( ( TimeSpan Delay , TimeSpan Period ) timerOptions , CancellationToken cancellationToken )
135
179
{
136
180
// Forcibly yield - we want to unblock the timer thread.
137
181
await Task . Yield ( ) ;
138
182
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
+
139
191
// 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 ) ;
141
193
142
194
var publishers = _publishers ;
143
195
var tasks = new Task [ publishers . Length ] ;
0 commit comments