diff --git a/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheck.cs b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheck.cs
index 172c041..d0a150d 100644
--- a/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheck.cs
+++ b/ZB.MOM.WW.Health/src/ZB.MOM.WW.Health.EntityFrameworkCore/DatabaseHealthCheck.cs
@@ -24,6 +24,11 @@ namespace ZB.MOM.WW.Health.EntityFrameworkCore;
/// disposed context); otherwise a scoped is resolved from a new DI
/// scope. Recommended registration tag: ZbHealthTags.Ready (applied by the registrant).
///
+///
+/// The scoped-resolution path is safe for AddDbContextPool: disposing the
+/// returns the pooled context to the pool rather than destroying it,
+/// so no pooled instance is prematurely discarded.
+///
///
/// The EF Core to probe.
public sealed class DatabaseHealthCheck : IHealthCheck
@@ -59,7 +64,11 @@ public sealed class DatabaseHealthCheck : 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)
diff --git a/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests/DatabaseHealthCheckTests.cs b/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests/DatabaseHealthCheckTests.cs
index 6cb81df..f3412a7 100644
--- a/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests/DatabaseHealthCheckTests.cs
+++ b/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests/DatabaseHealthCheckTests.cs
@@ -10,9 +10,10 @@ namespace ZB.MOM.WW.Health.EntityFrameworkCore.Tests;
/// Verifies against a real SQLite database (in-memory,
/// connection kept open) so the CanConnectAsync semantics exercise an actual provider:
/// reachable → Healthy, unopenable connection → Unhealthy (no throw escapes), a custom
-/// that queries → Healthy, and a
-/// throwing ProbeQuery → Unhealthy. Both the and
-/// the scoped-TContext resolution paths are covered.
+/// that queries → Healthy, a
+/// throwing ProbeQuery → Unhealthy, and a timed-out probe → Unhealthy. Both the
+/// and the scoped-TContext resolution paths
+/// are covered.
///
public sealed class DatabaseHealthCheckTests
{
@@ -43,7 +44,7 @@ public sealed class DatabaseHealthCheckTests
/// SQLite connection (and creates the schema). When is true the
/// context is registered via AddDbContextFactory; otherwise via AddDbContext (scoped).
///
- private static IServiceProvider BuildProvider(SqliteConnection connection, bool useFactory)
+ private static ServiceProvider BuildProvider(SqliteConnection connection, bool useFactory)
{
connection.Open();
@@ -71,7 +72,7 @@ public sealed class DatabaseHealthCheckTests
public async Task ReachableContext_Healthy(bool useFactory)
{
using var connection = new SqliteConnection("DataSource=:memory:");
- var provider = BuildProvider(connection, useFactory);
+ await using var provider = BuildProvider(connection, useFactory);
var check = new DatabaseHealthCheck(provider);
@@ -88,7 +89,7 @@ public sealed class DatabaseHealthCheckTests
var services = new ServiceCollection();
services.AddDbContext(o => o.UseSqlite($"DataSource={bogusPath};Mode=ReadWrite"));
- var provider = services.BuildServiceProvider();
+ await using var provider = services.BuildServiceProvider();
var check = new DatabaseHealthCheck(provider);
@@ -101,7 +102,7 @@ public sealed class DatabaseHealthCheckTests
public async Task CustomProbeQuery_RunsQuery_Healthy()
{
using var connection = new SqliteConnection("DataSource=:memory:");
- var provider = BuildProvider(connection, useFactory: true);
+ await using var provider = BuildProvider(connection, useFactory: true);
var options = new DatabaseHealthCheckOptions
{
@@ -118,11 +119,32 @@ public sealed class DatabaseHealthCheckTests
public async Task ProbeQueryThrows_Unhealthy()
{
using var connection = new SqliteConnection("DataSource=:memory:");
- var provider = BuildProvider(connection, useFactory: false);
+ await using var provider = BuildProvider(connection, useFactory: false);
var options = new DatabaseHealthCheckOptions
{
- ProbeQuery = (_, _) => throw new InvalidOperationException("boom"),
+ // Use a faulted task rather than a synchronous throw to accurately model
+ // async probe delegates that encounter an error.
+ ProbeQuery = (_, _) => Task.FromException(new InvalidOperationException("boom")),
+ };
+ var check = new DatabaseHealthCheck(provider, options);
+
+ var result = await check.CheckHealthAsync(NewContext(), CancellationToken.None);
+
+ Assert.Equal(HealthStatus.Unhealthy, result.Status);
+ }
+
+ [Fact]
+ public async Task ProbeTimeout_Unhealthy()
+ {
+ using var connection = new SqliteConnection("DataSource=:memory:");
+ await using var provider = BuildProvider(connection, useFactory: true);
+
+ // Use a very short timeout and a probe that blocks indefinitely (until cancelled).
+ var options = new DatabaseHealthCheckOptions
+ {
+ Timeout = TimeSpan.FromMilliseconds(50),
+ ProbeQuery = async (_, ct) => await Task.Delay(Timeout.Infinite, ct),
};
var check = new DatabaseHealthCheck(provider, options);
diff --git a/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests.csproj b/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests.csproj
index 1f52ef9..f94b6ee 100644
--- a/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests.csproj
+++ b/ZB.MOM.WW.Health/tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests/ZB.MOM.WW.Health.EntityFrameworkCore.Tests.csproj
@@ -9,7 +9,6 @@
-