fix(auditlog,health): harden hosted-service shutdown against disposed CTS
The host does not guarantee IHostedService.StopAsync is driven before the DI container is disposed — WebApplicationFactory's teardown reaches Dispose first — so cancelling the internal CTS from StopAsync threw ObjectDisposedException and aborted the host's whole shutdown sequence. Four services shared the same copy-pasted lifecycle and the same two races: StopAsync cancelling an already- disposed CTS, and StartAsync reading _cts.Token lazily inside the Task.Run lambda, which faults the loop task the host awaits when Dispose wins that race. Each service now captures the token on the caller's thread, tolerates a disposed CTS, and cancels-before-disposing so the loop is always signalled and its pending Task.Delay sees a cancelled token rather than a dead source. SiteAuditBacklogReporter also gains the outer OperationCanceledException guard its sibling SiteAuditRetentionService already carried (arch-review 04 R2, R7), without which a shutdown landing mid-probe threw TaskCanceledException out of Host.StopAsync. Surfaced while verifying the Gitea #15 test-harness fix: in Host.Tests the aborted teardown skipped the fixture's env-var restore, contaminating every later test in the run. Refs: Gitea #15
This commit is contained in:
+21
@@ -10,6 +10,27 @@ namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests;
|
||||
/// </summary>
|
||||
public class SiteEventLogFailureCountReporterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StopAsync_AfterDispose_DoesNotThrow()
|
||||
{
|
||||
// Regression (Gitea #15 follow-up): Dispose tears down the CTS StopAsync
|
||||
// cancels, and the host does not guarantee StopAsync is driven before the DI
|
||||
// container is disposed. Cancel() on a disposed CTS throws, and letting that
|
||||
// escape an IHostedService aborts the host's whole shutdown sequence.
|
||||
var reporter = new SiteEventLogFailureCountReporter(
|
||||
failedWriteCountProvider: () => 0L,
|
||||
collector: new SiteHealthCollector(),
|
||||
logger: NullLogger<SiteEventLogFailureCountReporter>.Instance,
|
||||
refreshInterval: TimeSpan.FromHours(1));
|
||||
|
||||
await reporter.StartAsync(CancellationToken.None);
|
||||
reporter.Dispose();
|
||||
|
||||
// The assertion is the absence of ObjectDisposedException.
|
||||
await reporter.StopAsync(CancellationToken.None);
|
||||
reporter.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_ImmediatelyProbes_FailedWriteCount()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user