using System.Collections.Concurrent;
using Elsa.Common;
namespace Elsa.Workflows.Runtime.Services;
///
/// Default implementation of . Tracks active execution cycles with a
/// thread-safe dictionary and detects the FR-018 invariant violation (a source that reports
/// but initiates an execution cycle).
///
public sealed class ExecutionCycleRegistry : IExecutionCycleRegistry
{
private readonly ConcurrentDictionary _active = new();
private readonly IIngressSourceRegistry _sources;
private readonly ISystemClock _clock;
public ExecutionCycleRegistry(IIngressSourceRegistry sources, ISystemClock clock)
{
_sources = sources;
_clock = clock;
}
///
public int ActiveCount => _active.Count;
///
public ExecutionCycleHandle BeginCycle(string workflowInstanceId, string? ingressSourceName, CancellationToken linkedToken, Action? cancelCallback = null)
{
// FR-018: a source that reports Paused but starts an execution cycle is inconsistent — flip it to PauseFailed.
if (ingressSourceName is not null)
{
var snapshot = _sources.Snapshot().FirstOrDefault(s => s.Name == ingressSourceName);
if (snapshot is not null && snapshot.State == IngressSourceState.Paused)
{
// Fire-and-forget: flipping the recorded state is a local operation that does not await I/O.
_ = _sources.MarkPauseFailedAsync(ingressSourceName, reason: "delivered-while-paused");
}
}
var handle = new ExecutionCycleHandle(
id: Guid.NewGuid(),
workflowInstanceId: workflowInstanceId,
ingressSourceName: ingressSourceName,
startedAt: _clock.UtcNow,
linkedToken: linkedToken,
onDisposed: h => _active.TryRemove(h.Id, out _),
cancelCallback: cancelCallback);
_active[handle.Id] = handle;
return handle;
}
///
public IReadOnlyCollection ListActiveCycles() => _active.Values.ToArray();
}