diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs
index 3990bf81d..dddf8ea85 100644
--- a/src/apps/Elsa.Server.Web/Program.cs
+++ b/src/apps/Elsa.Server.Web/Program.cs
@@ -186,7 +186,11 @@ app.MapHealthChecks("/health/ready", new()
});
app.MapHealthChecks("/", new()
{
- Predicate = _ => false
+ ResultStatusCodes =
+ {
+ [HealthStatus.Degraded] = StatusCodes.Status503ServiceUnavailable,
+ [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
+ }
});
// Routing used for SignalR.
diff --git a/src/modules/Elsa.Workflows.Runtime/HealthChecks/ElsaWorkflowPersistenceHealthCheck.cs b/src/modules/Elsa.Workflows.Runtime/HealthChecks/ElsaWorkflowPersistenceHealthCheck.cs
index 55d5478fb..3c6dca7fb 100644
--- a/src/modules/Elsa.Workflows.Runtime/HealthChecks/ElsaWorkflowPersistenceHealthCheck.cs
+++ b/src/modules/Elsa.Workflows.Runtime/HealthChecks/ElsaWorkflowPersistenceHealthCheck.cs
@@ -18,11 +18,13 @@ public class ElsaWorkflowPersistenceHealthCheck(IServiceProvider serviceProvider
///
public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
- var probeResults = await Task.WhenAll(
- ProbeAsync("workflow-definitions", serviceProvider.GetService(), async (store, ct) => await store.FindAsync(new WorkflowDefinitionFilter { Id = ProbeId }, ct)),
- ProbeAsync("workflow-instances", serviceProvider.GetService(), async (store, ct) => await store.CountAsync(new WorkflowInstanceFilter { Id = ProbeId }, ct)),
- ProbeAsync("triggers", serviceProvider.GetService(), async (store, ct) => await store.FindAsync(new TriggerFilter { Id = ProbeId }, ct)),
- ProbeAsync("bookmark-queue", serviceProvider.GetService(), async (store, ct) => await store.FindAsync(new BookmarkQueueFilter { Id = ProbeId }, ct)));
+ var probeResults = new List
+ {
+ await ProbeAsync("workflow-definitions", serviceProvider.GetService(), async (store, ct) => await store.FindAsync(new WorkflowDefinitionFilter { Id = ProbeId }, ct)),
+ await ProbeAsync("workflow-instances", serviceProvider.GetService(), async (store, ct) => await store.CountAsync(new WorkflowInstanceFilter { Id = ProbeId }, ct)),
+ await ProbeAsync("triggers", serviceProvider.GetService(), async (store, ct) => await store.FindAsync(new TriggerFilter { Id = ProbeId }, ct)),
+ await ProbeAsync("bookmark-queue", serviceProvider.GetService(), async (store, ct) => await store.FindAsync(new BookmarkQueueFilter { Id = ProbeId }, ct))
+ };
var attemptedProbes = probeResults.Where(x => !x.Skipped).Select(x => x.StoreName).ToList();
var successfulProbes = probeResults.Where(x => !x.Skipped && x.Exception == null).Select(x => x.StoreName).ToList();
diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/HealthChecks/ElsaWorkflowPersistenceHealthCheckTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/HealthChecks/ElsaWorkflowPersistenceHealthCheckTests.cs
index 02fcd8f11..26c37cc95 100644
--- a/test/unit/Elsa.Workflows.Runtime.UnitTests/HealthChecks/ElsaWorkflowPersistenceHealthCheckTests.cs
+++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/HealthChecks/ElsaWorkflowPersistenceHealthCheckTests.cs
@@ -44,6 +44,25 @@ public class ElsaWorkflowPersistenceHealthCheckTests
Arg.Any());
}
+ [Fact]
+ public async Task ProbesStoresSequentially()
+ {
+ var tracker = new ProbeConcurrencyTracker();
+ _workflowDefinitionStore.FindAsync(Arg.Any(), Arg.Any())
+ .Returns(_ => TrackProbeAsync(tracker, null));
+ _workflowInstanceStore.CountAsync(Arg.Any(), Arg.Any())
+ .Returns(_ => new ValueTask(TrackProbeAsync(tracker, 0L)));
+ _triggerStore.FindAsync(Arg.Any(), Arg.Any())
+ .Returns(_ => new ValueTask(TrackProbeAsync(tracker, null)));
+ _bookmarkQueueStore.FindAsync(Arg.Any(), Arg.Any())
+ .Returns(_ => TrackProbeAsync(tracker, null));
+
+ var result = await _sut.CheckHealthAsync(new HealthCheckContext());
+
+ Assert.Equal(HealthStatus.Healthy, result.Status);
+ Assert.Equal(1, tracker.MaxConcurrentProbes);
+ }
+
[Fact]
public async Task ReturnsUnhealthyWithFailedStoreWhenAStoreProbeFails()
{
@@ -92,4 +111,44 @@ public class ElsaWorkflowPersistenceHealthCheckTests
Assert.Equal("workflow-definitions,workflow-instances,triggers,bookmark-queue", result.Data["skippedProbes"]);
Assert.False(result.Data.ContainsKey("probes"));
}
+
+ private static async Task TrackProbeAsync(ProbeConcurrencyTracker tracker, T result)
+ {
+ tracker.Enter();
+
+ try
+ {
+ await Task.Delay(10);
+ return result;
+ }
+ finally
+ {
+ tracker.Exit();
+ }
+ }
+
+ private sealed class ProbeConcurrencyTracker
+ {
+ private int _currentProbes;
+ private int _maxConcurrentProbes;
+
+ public int MaxConcurrentProbes => Volatile.Read(ref _maxConcurrentProbes);
+
+ public void Enter()
+ {
+ var currentProbes = Interlocked.Increment(ref _currentProbes);
+
+ while (true)
+ {
+ var maxConcurrentProbes = MaxConcurrentProbes;
+ if (currentProbes <= maxConcurrentProbes)
+ return;
+
+ if (Interlocked.CompareExchange(ref _maxConcurrentProbes, currentProbes, maxConcurrentProbes) == maxConcurrentProbes)
+ return;
+ }
+ }
+
+ public void Exit() => Interlocked.Decrement(ref _currentProbes);
+ }
}