using System.Collections.Concurrent; namespace ScadaLink.DeploymentManager; /// /// WP-3: Per-instance operation lock. Only one mutating operation (deploy, disable, enable, delete) /// may be in progress per instance at a time. Different instances can proceed in parallel. /// /// Implementation: ConcurrentDictionary of SemaphoreSlim(1,1) keyed by instance unique name. /// Lock released on completion, timeout, or failure. /// Lost on central failover (acceptable per design -- in-progress treated as failed). /// public class OperationLockManager { private readonly ConcurrentDictionary _locks = new(StringComparer.Ordinal); /// /// Acquires the operation lock for the given instance. Returns a disposable that releases the lock. /// Throws TimeoutException if the lock cannot be acquired within the timeout. /// public async Task AcquireAsync(string instanceUniqueName, TimeSpan timeout, CancellationToken cancellationToken = default) { var semaphore = _locks.GetOrAdd(instanceUniqueName, _ => new SemaphoreSlim(1, 1)); if (!await semaphore.WaitAsync(timeout, cancellationToken)) { throw new TimeoutException( $"Could not acquire operation lock for instance '{instanceUniqueName}' within {timeout.TotalSeconds}s. " + "Another mutating operation is in progress."); } return new LockRelease(semaphore); } /// /// Checks whether a lock is currently held for the given instance (for diagnostics). /// public bool IsLocked(string instanceUniqueName) { return _locks.TryGetValue(instanceUniqueName, out var semaphore) && semaphore.CurrentCount == 0; } private sealed class LockRelease : IDisposable { private readonly SemaphoreSlim _semaphore; private int _disposed; public LockRelease(SemaphoreSlim semaphore) => _semaphore = semaphore; public void Dispose() { if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) { _semaphore.Release(); } } } }