diff --git a/src/modules/Elsa.Workflows.Runtime/HealthChecks/ElsaWorkflowPersistenceHealthCheck.cs b/src/modules/Elsa.Workflows.Runtime/HealthChecks/ElsaWorkflowPersistenceHealthCheck.cs
index 0a735c9b5..6d0b1a52e 100644
--- a/src/modules/Elsa.Workflows.Runtime/HealthChecks/ElsaWorkflowPersistenceHealthCheck.cs
+++ b/src/modules/Elsa.Workflows.Runtime/HealthChecks/ElsaWorkflowPersistenceHealthCheck.cs
@@ -2,6 +2,7 @@ using Elsa.Common;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Runtime.Filters;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Elsa.Workflows.Runtime.HealthChecks;
@@ -9,11 +10,7 @@ namespace Elsa.Workflows.Runtime.HealthChecks;
///
/// Performs small read-only probes against the workflow management and runtime stores.
///
-public class ElsaWorkflowPersistenceHealthCheck(
- IWorkflowDefinitionStore workflowDefinitionStore,
- IWorkflowInstanceStore workflowInstanceStore,
- ITriggerStore triggerStore,
- IBookmarkQueueStore bookmarkQueueStore) : IHealthCheck
+public class ElsaWorkflowPersistenceHealthCheck(IServiceProvider serviceProvider) : IHealthCheck
{
private const string ProbeId = "__elsa_health_check_probe__";
@@ -21,33 +18,58 @@ public class ElsaWorkflowPersistenceHealthCheck(
public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var failedStore = "";
+ var probes = new List();
+ var skippedProbes = new List();
try
{
- await ProbeAsync("workflow-definitions", async ct => await workflowDefinitionStore.FindAsync(new WorkflowDefinitionFilter { Id = ProbeId }, ct));
- await ProbeAsync("workflow-instances", async ct => await workflowInstanceStore.CountAsync(new WorkflowInstanceFilter { Id = ProbeId }, ct));
- await ProbeAsync("triggers", async ct => await triggerStore.FindAsync(new TriggerFilter { Id = ProbeId }, ct));
- await ProbeAsync("bookmark-queue", async ct => await bookmarkQueueStore.FindAsync(new BookmarkQueueFilter { Id = ProbeId }, ct));
+ 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));
- return HealthCheckResult.Healthy("Elsa workflow stores are reachable.", new Dictionary
- {
- ["category"] = "persistence",
- ["probes"] = "workflow-definitions,workflow-instances,triggers,bookmark-queue"
- });
+ var data = CreateData();
+ return probes.Count == 0
+ ? HealthCheckResult.Degraded("No Elsa workflow persistence stores are registered.", data: data)
+ : HealthCheckResult.Healthy("Elsa workflow stores are reachable.", data);
}
catch (Exception e) when (!e.IsFatal())
{
return HealthCheckResult.Unhealthy($"Elsa workflow store '{failedStore}' is not reachable.", e, new Dictionary
{
["category"] = "persistence",
- ["failedStore"] = failedStore
+ ["failedStore"] = failedStore,
+ ["failedProbe"] = failedStore
});
}
- async Task ProbeAsync(string store, Func probe)
+ async Task ProbeAsync(string storeName, TStore? store, Func probe) where TStore : class
{
- failedStore = store;
- await probe(cancellationToken);
+ if (store == null)
+ {
+ skippedProbes.Add(storeName);
+ return;
+ }
+
+ failedStore = storeName;
+ probes.Add(storeName);
+ await probe(store, cancellationToken);
+ }
+
+ Dictionary CreateData()
+ {
+ var data = new Dictionary
+ {
+ ["category"] = "persistence"
+ };
+
+ if (probes.Count > 0)
+ data["probes"] = string.Join(",", probes);
+
+ if (skippedProbes.Count > 0)
+ data["skippedProbes"] = string.Join(",", skippedProbes);
+
+ return data;
}
}
}
diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/HealthChecks/ElsaWorkflowPersistenceHealthCheckTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/HealthChecks/ElsaWorkflowPersistenceHealthCheckTests.cs
index 893bb3c5a..705bb7f5b 100644
--- a/test/unit/Elsa.Workflows.Runtime.UnitTests/HealthChecks/ElsaWorkflowPersistenceHealthCheckTests.cs
+++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/HealthChecks/ElsaWorkflowPersistenceHealthCheckTests.cs
@@ -9,6 +9,7 @@ namespace Elsa.Workflows.Runtime.UnitTests.HealthChecks;
public class ElsaWorkflowPersistenceHealthCheckTests
{
+ private readonly IServiceProvider _serviceProvider = Substitute.For();
private readonly IWorkflowDefinitionStore _workflowDefinitionStore = Substitute.For();
private readonly IWorkflowInstanceStore _workflowInstanceStore = Substitute.For();
private readonly ITriggerStore _triggerStore = Substitute.For();
@@ -21,7 +22,11 @@ public class ElsaWorkflowPersistenceHealthCheckTests
_workflowInstanceStore.CountAsync(Arg.Any(), Arg.Any()).Returns(new ValueTask(0));
_triggerStore.FindAsync(Arg.Any(), Arg.Any()).Returns(new ValueTask((Elsa.Workflows.Runtime.Entities.StoredTrigger?)null));
_bookmarkQueueStore.FindAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(null));
- _sut = new ElsaWorkflowPersistenceHealthCheck(_workflowDefinitionStore, _workflowInstanceStore, _triggerStore, _bookmarkQueueStore);
+ _serviceProvider.GetService(typeof(IWorkflowDefinitionStore)).Returns(_workflowDefinitionStore);
+ _serviceProvider.GetService(typeof(IWorkflowInstanceStore)).Returns(_workflowInstanceStore);
+ _serviceProvider.GetService(typeof(ITriggerStore)).Returns(_triggerStore);
+ _serviceProvider.GetService(typeof(IBookmarkQueueStore)).Returns(_bookmarkQueueStore);
+ _sut = new ElsaWorkflowPersistenceHealthCheck(_serviceProvider);
}
[Fact]
@@ -44,5 +49,20 @@ public class ElsaWorkflowPersistenceHealthCheckTests
Assert.Equal("Elsa workflow store 'triggers' is not reachable.", result.Description);
Assert.Equal("persistence", result.Data["category"]);
Assert.Equal("triggers", result.Data["failedStore"]);
+ Assert.Equal("triggers", result.Data["failedProbe"]);
+ }
+
+ [Fact]
+ public async Task ReturnsHealthyWithSkippedProbesWhenOptionalManagementStoresAreMissing()
+ {
+ _serviceProvider.GetService(typeof(IWorkflowDefinitionStore)).Returns((object?)null);
+ _serviceProvider.GetService(typeof(IWorkflowInstanceStore)).Returns((object?)null);
+
+ var result = await _sut.CheckHealthAsync(new HealthCheckContext());
+
+ Assert.Equal(HealthStatus.Healthy, result.Status);
+ Assert.Equal("persistence", result.Data["category"]);
+ Assert.Equal("triggers,bookmark-queue", result.Data["probes"]);
+ Assert.Equal("workflow-definitions,workflow-instances", result.Data["skippedProbes"]);
}
}