using System.Collections.Concurrent; namespace w4c_workflows.Services; /// /// Serializes concurrent workflow syncs for the same (tenant, repo) within /// one process. Two overlapping POST /sync calls would otherwise both /// insert the same deterministic workflow/task ids and collide on the primary key /// (DbUpdateException). The gate makes the second call wait, so it observes /// the first call's committed rows and upserts instead of duplicating. /// /// For multi-replica deployments this is combined with the distributed /// lease in . /// public sealed class SyncGate { private readonly ConcurrentDictionary _gates = new(StringComparer.Ordinal); public async Task AcquireAsync(string tenantId, string repoName, CancellationToken ct) { var gate = _gates.GetOrAdd(tenantId + "|" + repoName, _ => new SemaphoreSlim(1, 1)); await gate.WaitAsync(ct); return new Releaser(gate); } private sealed class Releaser(SemaphoreSlim gate) : IDisposable { private int _released; public void Dispose() { if (Interlocked.Exchange(ref _released, 1) == 0) gate.Release(); } } }