Address health check review feedback

This commit is contained in:
Sipke Schoorstra 2026-05-21 01:16:36 +02:00
parent bcccb80ad5
commit a0d6f6b24b
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
3 changed files with 71 additions and 6 deletions

View file

@ -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.

View file

@ -18,11 +18,13 @@ public class ElsaWorkflowPersistenceHealthCheck(IServiceProvider serviceProvider
/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var probeResults = await Task.WhenAll(
ProbeAsync("workflow-definitions", serviceProvider.GetService<IWorkflowDefinitionStore>(), async (store, ct) => await store.FindAsync(new WorkflowDefinitionFilter { Id = ProbeId }, ct)),
ProbeAsync("workflow-instances", serviceProvider.GetService<IWorkflowInstanceStore>(), async (store, ct) => await store.CountAsync(new WorkflowInstanceFilter { Id = ProbeId }, ct)),
ProbeAsync("triggers", serviceProvider.GetService<ITriggerStore>(), async (store, ct) => await store.FindAsync(new TriggerFilter { Id = ProbeId }, ct)),
ProbeAsync("bookmark-queue", serviceProvider.GetService<IBookmarkQueueStore>(), async (store, ct) => await store.FindAsync(new BookmarkQueueFilter { Id = ProbeId }, ct)));
var probeResults = new List<ProbeResult>
{
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))
};
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();

View file

@ -44,6 +44,25 @@ public class ElsaWorkflowPersistenceHealthCheckTests
Arg.Any<CancellationToken>());
}
[Fact]
public async Task ProbesStoresSequentially()
{
var tracker = new ProbeConcurrencyTracker();
_workflowDefinitionStore.FindAsync(Arg.Any<WorkflowDefinitionFilter>(), Arg.Any<CancellationToken>())
.Returns(_ => TrackProbeAsync<Elsa.Workflows.Management.Entities.WorkflowDefinition?>(tracker, null));
_workflowInstanceStore.CountAsync(Arg.Any<WorkflowInstanceFilter>(), Arg.Any<CancellationToken>())
.Returns(_ => new ValueTask<long>(TrackProbeAsync(tracker, 0L)));
_triggerStore.FindAsync(Arg.Any<TriggerFilter>(), Arg.Any<CancellationToken>())
.Returns(_ => new ValueTask<Elsa.Workflows.Runtime.Entities.StoredTrigger?>(TrackProbeAsync<Elsa.Workflows.Runtime.Entities.StoredTrigger?>(tracker, null)));
_bookmarkQueueStore.FindAsync(Arg.Any<BookmarkQueueFilter>(), Arg.Any<CancellationToken>())
.Returns(_ => TrackProbeAsync<Elsa.Workflows.Runtime.Entities.BookmarkQueueItem?>(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<T> TrackProbeAsync<T>(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);
}
}