Skip to content

Commit fe4df69

Browse files
bhugotBenjamin HUGOT
andauthored
Run every health check in its own scope
Co-authored-by: Benjamin HUGOT <[email protected]>
1 parent e98b045 commit fe4df69

File tree

2 files changed

+146
-75
lines changed

2 files changed

+146
-75
lines changed

src/HealthChecks/HealthChecks/src/DefaultHealthCheckService.cs

Lines changed: 75 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,15 @@ public override async Task<HealthReport> CheckHealthAsync(
5050

5151
var tasks = new Task<HealthReportEntry>[registrations.Count];
5252
var index = 0;
53-
using (var scope = _scopeFactory.CreateScope())
54-
{
55-
foreach (var registration in registrations)
56-
{
57-
tasks[index++] = Task.Run(() => RunCheckAsync(scope, registration, cancellationToken), cancellationToken);
58-
}
5953

60-
await Task.WhenAll(tasks).ConfigureAwait(false);
54+
foreach (var registration in registrations)
55+
{
56+
tasks[index++] = Task.Run(() => RunCheckAsync(registration, cancellationToken), cancellationToken);
6157
}
6258

59+
await Task.WhenAll(tasks).ConfigureAwait(false);
60+
61+
6362
index = 0;
6463
var entries = new Dictionary<string, HealthReportEntry>(StringComparer.OrdinalIgnoreCase);
6564
foreach (var registration in registrations)
@@ -73,85 +72,88 @@ public override async Task<HealthReport> CheckHealthAsync(
7372
return report;
7473
}
7574

76-
private async Task<HealthReportEntry> RunCheckAsync(IServiceScope scope, HealthCheckRegistration registration, CancellationToken cancellationToken)
75+
private async Task<HealthReportEntry> RunCheckAsync(HealthCheckRegistration registration, CancellationToken cancellationToken)
7776
{
7877
cancellationToken.ThrowIfCancellationRequested();
7978

80-
var healthCheck = registration.Factory(scope.ServiceProvider);
81-
82-
// If the health check does things like make Database queries using EF or backend HTTP calls,
83-
// it may be valuable to know that logs it generates are part of a health check. So we start a scope.
84-
using (_logger.BeginScope(new HealthCheckLogScope(registration.Name)))
79+
using (var scope = _scopeFactory.CreateScope())
8580
{
86-
var stopwatch = ValueStopwatch.StartNew();
87-
var context = new HealthCheckContext { Registration = registration };
88-
89-
Log.HealthCheckBegin(_logger, registration);
81+
var healthCheck = registration.Factory(scope.ServiceProvider);
9082

91-
HealthReportEntry entry;
92-
CancellationTokenSource? timeoutCancellationTokenSource = null;
93-
try
83+
// If the health check does things like make Database queries using EF or backend HTTP calls,
84+
// it may be valuable to know that logs it generates are part of a health check. So we start a scope.
85+
using (_logger.BeginScope(new HealthCheckLogScope(registration.Name)))
9486
{
95-
HealthCheckResult result;
87+
var stopwatch = ValueStopwatch.StartNew();
88+
var context = new HealthCheckContext { Registration = registration };
9689

97-
var checkCancellationToken = cancellationToken;
98-
if (registration.Timeout > TimeSpan.Zero)
90+
Log.HealthCheckBegin(_logger, registration);
91+
92+
HealthReportEntry entry;
93+
CancellationTokenSource? timeoutCancellationTokenSource = null;
94+
try
9995
{
100-
timeoutCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
101-
timeoutCancellationTokenSource.CancelAfter(registration.Timeout);
102-
checkCancellationToken = timeoutCancellationTokenSource.Token;
96+
HealthCheckResult result;
97+
98+
var checkCancellationToken = cancellationToken;
99+
if (registration.Timeout > TimeSpan.Zero)
100+
{
101+
timeoutCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
102+
timeoutCancellationTokenSource.CancelAfter(registration.Timeout);
103+
checkCancellationToken = timeoutCancellationTokenSource.Token;
104+
}
105+
106+
result = await healthCheck.CheckHealthAsync(context, checkCancellationToken).ConfigureAwait(false);
107+
108+
var duration = stopwatch.GetElapsedTime();
109+
110+
entry = new HealthReportEntry(
111+
status: result.Status,
112+
description: result.Description,
113+
duration: duration,
114+
exception: result.Exception,
115+
data: result.Data,
116+
tags: registration.Tags);
117+
118+
Log.HealthCheckEnd(_logger, registration, entry, duration);
119+
Log.HealthCheckData(_logger, registration, entry);
120+
}
121+
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
122+
{
123+
var duration = stopwatch.GetElapsedTime();
124+
entry = new HealthReportEntry(
125+
status: registration.FailureStatus,
126+
description: "A timeout occurred while running check.",
127+
duration: duration,
128+
exception: ex,
129+
data: null,
130+
tags: registration.Tags);
131+
132+
Log.HealthCheckError(_logger, registration, ex, duration);
103133
}
104134

105-
result = await healthCheck.CheckHealthAsync(context, checkCancellationToken).ConfigureAwait(false);
106-
107-
var duration = stopwatch.GetElapsedTime();
108-
109-
entry = new HealthReportEntry(
110-
status: result.Status,
111-
description: result.Description,
112-
duration: duration,
113-
exception: result.Exception,
114-
data: result.Data,
115-
tags: registration.Tags);
116-
117-
Log.HealthCheckEnd(_logger, registration, entry, duration);
118-
Log.HealthCheckData(_logger, registration, entry);
119-
}
120-
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
121-
{
122-
var duration = stopwatch.GetElapsedTime();
123-
entry = new HealthReportEntry(
124-
status: registration.FailureStatus,
125-
description: "A timeout occurred while running check.",
126-
duration: duration,
127-
exception: ex,
128-
data: null,
129-
tags: registration.Tags);
130-
131-
Log.HealthCheckError(_logger, registration, ex, duration);
132-
}
135+
// Allow cancellation to propagate if it's not a timeout.
136+
catch (Exception ex) when (ex as OperationCanceledException == null)
137+
{
138+
var duration = stopwatch.GetElapsedTime();
139+
entry = new HealthReportEntry(
140+
status: registration.FailureStatus,
141+
description: ex.Message,
142+
duration: duration,
143+
exception: ex,
144+
data: null,
145+
tags: registration.Tags);
146+
147+
Log.HealthCheckError(_logger, registration, ex, duration);
148+
}
133149

134-
// Allow cancellation to propagate if it's not a timeout.
135-
catch (Exception ex) when (ex as OperationCanceledException == null)
136-
{
137-
var duration = stopwatch.GetElapsedTime();
138-
entry = new HealthReportEntry(
139-
status: registration.FailureStatus,
140-
description: ex.Message,
141-
duration: duration,
142-
exception: ex,
143-
data: null,
144-
tags: registration.Tags);
145-
146-
Log.HealthCheckError(_logger, registration, ex, duration);
147-
}
150+
finally
151+
{
152+
timeoutCancellationTokenSource?.Dispose();
153+
}
148154

149-
finally
150-
{
151-
timeoutCancellationTokenSource?.Dispose();
155+
return entry;
152156
}
153-
154-
return entry;
155157
}
156158
}
157159

src/HealthChecks/HealthChecks/test/DefaultHealthCheckServiceTest.cs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,37 @@ public async Task CheckHealthAsync_CheckCanDependOnScopedService()
402402
});
403403
}
404404

405+
[Fact]
406+
// related to issue https://github.com/dotnet/aspnetcore/issues/14453
407+
public async Task CheckHealthAsync_CheckCanDependOnScopedService_per_check()
408+
{
409+
// Arrange
410+
var service = CreateHealthChecksService(b =>
411+
{
412+
b.Services.AddScoped<CantBeMultiThreadedService>();
413+
414+
b.AddCheck<CheckWithServiceNotMultiThreadDependency>("Test");
415+
b.AddCheck<CheckWithServiceNotMultiThreadDependency>("Test2");
416+
});
417+
418+
// Act
419+
var results = await service.CheckHealthAsync();
420+
421+
// Assert
422+
Assert.Collection(
423+
results.Entries,
424+
actual =>
425+
{
426+
Assert.Equal("Test", actual.Key);
427+
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
428+
},
429+
actual =>
430+
{
431+
Assert.Equal("Test2", actual.Key);
432+
Assert.Equal(HealthStatus.Healthy, actual.Value.Status);
433+
});
434+
}
435+
405436
[Fact]
406437
public async Task CheckHealthAsync_CheckCanDependOnSingletonService()
407438
{
@@ -532,7 +563,7 @@ public void CheckHealthAsync_WorksInSingleThreadedSyncContext()
532563
// Assert
533564
Assert.False(hangs);
534565
}
535-
566+
536567
[Fact]
537568
public async Task CheckHealthAsync_WithFailureStatus()
538569
{
@@ -584,6 +615,20 @@ private static DefaultHealthCheckService CreateHealthChecksService(Action<IHealt
584615

585616
private class AnotherService { }
586617

618+
private class CantBeMultiThreadedService
619+
{
620+
private readonly object _lock = new();
621+
private bool _wasUsed;
622+
public void Check()
623+
{
624+
lock (_lock)
625+
{
626+
if (_wasUsed) throw new InvalidOperationException("Should only used once");
627+
_wasUsed = true;
628+
}
629+
}
630+
}
631+
587632
private class CheckWithServiceDependency : IHealthCheck
588633
{
589634
public CheckWithServiceDependency(AnotherService _)
@@ -596,6 +641,30 @@ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, Canc
596641
}
597642
}
598643

644+
private class CheckWithServiceNotMultiThreadDependency : IHealthCheck
645+
{
646+
private readonly CantBeMultiThreadedService _service;
647+
648+
public CheckWithServiceNotMultiThreadDependency(CantBeMultiThreadedService service)
649+
{
650+
_service = service;
651+
}
652+
653+
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
654+
{
655+
try
656+
{
657+
_service.Check();
658+
return Task.FromResult(HealthCheckResult.Healthy());
659+
}
660+
catch (InvalidOperationException e)
661+
{
662+
return Task.FromResult(HealthCheckResult.Unhealthy("failed", e));
663+
}
664+
665+
}
666+
}
667+
599668
private class NameCapturingCheck : IHealthCheck
600669
{
601670
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
@@ -607,7 +676,7 @@ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, Canc
607676
return Task.FromResult(HealthCheckResult.Healthy(data: data));
608677
}
609678
}
610-
679+
611680
private class FailCapturingCheck : IHealthCheck
612681
{
613682
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)

0 commit comments

Comments
 (0)