using System.Collections.Concurrent; namespace WebhookServer.Core.Execution; /// /// Holds one per endpoint. When an endpoint is configured /// with Serialize=true, the executor must acquire its semaphore before running and /// release after — guaranteeing at-most-one concurrent run per endpoint. /// public sealed class ConcurrencyGate { private readonly ConcurrentDictionary _gates = new(); public async Task AcquireAsync(Guid endpointId, CancellationToken ct) { var sem = _gates.GetOrAdd(endpointId, _ => new SemaphoreSlim(1, 1)); await sem.WaitAsync(ct).ConfigureAwait(false); return new Releaser(sem); } public void Forget(Guid endpointId) { if (_gates.TryRemove(endpointId, out var sem)) sem.Dispose(); } private sealed class Releaser : IDisposable { private SemaphoreSlim? _sem; public Releaser(SemaphoreSlim sem) => _sem = sem; public void Dispose() { var sem = Interlocked.Exchange(ref _sem, null); sem?.Release(); } } }