Guard persistence health check store probes
This commit is contained in:
parent
a8390d8d64
commit
481c1aaa81
|
|
@ -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;
|
|||
/// <summary>
|
||||
/// Performs small read-only probes against the workflow management and runtime stores.
|
||||
/// </summary>
|
||||
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<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var failedStore = "";
|
||||
var probes = new List<string>();
|
||||
var skippedProbes = new List<string>();
|
||||
|
||||
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<IWorkflowDefinitionStore>(), async (store, ct) => await store.FindAsync(new WorkflowDefinitionFilter { Id = ProbeId }, ct));
|
||||
await ProbeAsync("workflow-instances", serviceProvider.GetService<IWorkflowInstanceStore>(), async (store, ct) => await store.CountAsync(new WorkflowInstanceFilter { Id = ProbeId }, ct));
|
||||
await ProbeAsync("triggers", serviceProvider.GetService<ITriggerStore>(), async (store, ct) => await store.FindAsync(new TriggerFilter { Id = ProbeId }, ct));
|
||||
await ProbeAsync("bookmark-queue", serviceProvider.GetService<IBookmarkQueueStore>(), async (store, ct) => await store.FindAsync(new BookmarkQueueFilter { Id = ProbeId }, ct));
|
||||
|
||||
return HealthCheckResult.Healthy("Elsa workflow stores are reachable.", new Dictionary<string, object>
|
||||
{
|
||||
["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<string, object>
|
||||
{
|
||||
["category"] = "persistence",
|
||||
["failedStore"] = failedStore
|
||||
["failedStore"] = failedStore,
|
||||
["failedProbe"] = failedStore
|
||||
});
|
||||
}
|
||||
|
||||
async Task ProbeAsync(string store, Func<CancellationToken, Task> probe)
|
||||
async Task ProbeAsync<TStore>(string storeName, TStore? store, Func<TStore, CancellationToken, Task> 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<string, object> CreateData()
|
||||
{
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["category"] = "persistence"
|
||||
};
|
||||
|
||||
if (probes.Count > 0)
|
||||
data["probes"] = string.Join(",", probes);
|
||||
|
||||
if (skippedProbes.Count > 0)
|
||||
data["skippedProbes"] = string.Join(",", skippedProbes);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ namespace Elsa.Workflows.Runtime.UnitTests.HealthChecks;
|
|||
|
||||
public class ElsaWorkflowPersistenceHealthCheckTests
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider = Substitute.For<IServiceProvider>();
|
||||
private readonly IWorkflowDefinitionStore _workflowDefinitionStore = Substitute.For<IWorkflowDefinitionStore>();
|
||||
private readonly IWorkflowInstanceStore _workflowInstanceStore = Substitute.For<IWorkflowInstanceStore>();
|
||||
private readonly ITriggerStore _triggerStore = Substitute.For<ITriggerStore>();
|
||||
|
|
@ -21,7 +22,11 @@ public class ElsaWorkflowPersistenceHealthCheckTests
|
|||
_workflowInstanceStore.CountAsync(Arg.Any<WorkflowInstanceFilter>(), Arg.Any<CancellationToken>()).Returns(new ValueTask<long>(0));
|
||||
_triggerStore.FindAsync(Arg.Any<TriggerFilter>(), Arg.Any<CancellationToken>()).Returns(new ValueTask<Elsa.Workflows.Runtime.Entities.StoredTrigger?>((Elsa.Workflows.Runtime.Entities.StoredTrigger?)null));
|
||||
_bookmarkQueueStore.FindAsync(Arg.Any<BookmarkQueueFilter>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult<Elsa.Workflows.Runtime.Entities.BookmarkQueueItem?>(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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue