using System.Collections.Concurrent;
using Elsa.Common;
namespace Elsa.Workflows.Runtime.Services;
///
/// Default implementation of . Backed by a
/// keyed by source name, with atomic state transitions.
///
public sealed class IngressSourceRegistry : IIngressSourceRegistry
{
private readonly ConcurrentDictionary _entries = new(StringComparer.Ordinal);
private readonly Lazy> _sourcesFactory;
private readonly ISystemClock _clock;
private readonly object _materializeSync = new();
private volatile bool _materialized;
///
/// Creates the registry. The is materialized lazily so adapter
/// implementations can take a direct dependency without creating a DI cycle
/// (the signal depends on , which depends on this registry, which depends on
/// IEnumerable<IIngressSource>). The first call that requires materializes
/// the enumerable; subsequent calls reuse the snapshot.
///
public IngressSourceRegistry(Lazy> sourcesFactory, ISystemClock clock)
{
_sourcesFactory = sourcesFactory;
_clock = clock;
}
///
public IReadOnlyCollection Sources
{
get
{
EnsureMaterialized();
return _entries.Values.Select(e => e.Source).ToArray();
}
}
private void EnsureMaterialized()
{
// Double-checked lock: a `_entries.Count > 0` guard is not atomic with the population loop, so concurrent
// first callers could both observe an empty dictionary, both iterate _sourcesFactory.Value, and both call
// TryAdd — losing the duplicate-name guarantee and crashing the second caller's execution cycle start with
// "Duplicate ingress source registration". A short lock around the one-time population is sufficient;
// steady-state reads short-circuit on the volatile flag without taking the lock.
if (_materialized) return;
lock (_materializeSync)
{
if (_materialized) return;
foreach (var source in _sourcesFactory.Value)
{
if (!_entries.TryAdd(source.Name, new Entry(source)))
throw new InvalidOperationException($"Duplicate ingress source registration for name '{source.Name}'.");
}
_materialized = true;
}
}
///
public IReadOnlyCollection Snapshot()
{
EnsureMaterialized();
return _entries.Values
.Select(e => new IngressSourceSnapshot(e.Source.Name, e.State, e.LastError, e.LastTransitionAt))
.ToArray();
}
///
public ValueTask MarkPauseFailedAsync(string name, string reason, Exception? error = null)
{
EnsureMaterialized();
if (_entries.TryGetValue(name, out var entry))
{
lock (entry.Sync)
{
entry.State = IngressSourceState.PauseFailed;
entry.LastError = error ?? new InvalidOperationException(reason);
entry.LastTransitionAt = _clock.UtcNow;
}
}
return ValueTask.CompletedTask;
}
///
public void RecordTransition(string name, IngressSourceState newState, Exception? error = null)
{
EnsureMaterialized();
if (_entries.TryGetValue(name, out var entry))
{
lock (entry.Sync)
{
entry.State = newState;
entry.LastError = error;
entry.LastTransitionAt = _clock.UtcNow;
}
}
}
internal IngressSourceState GetState(string name)
{
EnsureMaterialized();
return _entries.TryGetValue(name, out var e) ? e.State : IngressSourceState.Running;
}
private sealed class Entry(IIngressSource source)
{
public readonly object Sync = new();
public IIngressSource Source { get; } = source;
public IngressSourceState State { get; set; } = IngressSourceState.Running;
public Exception? LastError { get; set; }
public DateTimeOffset? LastTransitionAt { get; set; }
}
}