refactor(health.ef): review polish (timer release, timeout test, provider disposal, drop unused dep)

- Eagerly call CancelAfter(InfiniteTimeSpan) after a successful probe so the pending OS
  timer is released on the happy path rather than held for the full timeout window.
- Add ProbeTimeout_Unhealthy test: 50 ms timeout with an infinite-blocking probe delegate
  asserts Unhealthy, covering the timeout code path.
- Fix ProbeQueryThrows_Unhealthy to use Task.FromException rather than a synchronous throw,
  accurately modelling a faulted async delegate.
- Wrap all BuildServiceProvider() results in await using so ServiceProvider is disposed
  after each test (no DI provider leak).
- Remove unused Microsoft.EntityFrameworkCore.InMemory package reference; tests use
  SQLite only (InMemory CanConnect semantics differ and the package was not exercised).
- Add <remarks> to DatabaseHealthCheck<TContext> noting the scoped-resolution path is
  safe for AddDbContextPool (scope dispose returns context to pool, not destroys it).
This commit is contained in:
Joseph Doherty
2026-06-01 07:03:16 -04:00
parent aa2251b93d
commit edbc79204f
3 changed files with 41 additions and 11 deletions
@@ -24,6 +24,11 @@ namespace ZB.MOM.WW.Health.EntityFrameworkCore;
/// disposed context); otherwise a scoped <typeparamref name="TContext"/> is resolved from a new DI
/// scope. Recommended registration tag: <c>ZbHealthTags.Ready</c> (applied by the registrant).
/// </para>
/// <para>
/// The scoped-resolution path is safe for <c>AddDbContextPool</c>: disposing the
/// <see cref="IServiceScope"/> returns the pooled context to the pool rather than destroying it,
/// so no pooled instance is prematurely discarded.
/// </para>
/// </remarks>
/// <typeparam name="TContext">The EF Core <see cref="DbContext"/> to probe.</typeparam>
public sealed class DatabaseHealthCheck<TContext> : IHealthCheck
@@ -59,7 +64,11 @@ public sealed class DatabaseHealthCheck<TContext> : IHealthCheck
try
{
return await ProbeAsync(timeoutCts.Token).ConfigureAwait(false);
var result = await ProbeAsync(timeoutCts.Token).ConfigureAwait(false);
// Eagerly release the pending timer on the happy path so the OS timer
// resource is not held for the full timeout duration.
timeoutCts.CancelAfter(Timeout.InfiniteTimeSpan);
return result;
}
catch (OperationCanceledException ex)
when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)