using System.Collections.Concurrent; namespace ZB.MOM.WW.ScadaBridge.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 ref-counted SemaphoreSlim(1,1) keyed by instance /// unique name. The lock is released on completion, timeout, or failure. /// Lost on central failover (acceptable per design -- in-progress treated as failed). /// /// DeploymentManager-005: each entry is ref-counted. The semaphore is created on the /// first acquire/wait, shared while there are waiters or a holder, and removed + /// d when the last reference is released — so the dictionary /// does not accumulate one kernel wait handle per distinct instance name forever. /// public class OperationLockManager { private readonly object _gate = new(); private readonly Dictionary _locks = new(StringComparer.Ordinal); /// /// Number of lock entries currently tracked. Used for diagnostics and to /// verify that semaphores are reclaimed (DeploymentManager-005). /// public int TrackedLockCount { get { lock (_gate) { return _locks.Count; } } } /// /// 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. /// /// The unique name of the instance to lock. /// Maximum time to wait for the lock before throwing . /// Cancellation token to abort the wait. /// An that releases the lock when disposed. public async Task AcquireAsync(string instanceUniqueName, TimeSpan timeout, CancellationToken cancellationToken = default) { // Reserve a reference (creating the entry if needed) BEFORE waiting, so a // concurrent waiter for the same instance shares the same semaphore and // the entry survives until every waiter/holder has released it. LockEntry entry; lock (_gate) { if (!_locks.TryGetValue(instanceUniqueName, out entry!)) { entry = new LockEntry(); _locks[instanceUniqueName] = entry; } entry.RefCount++; } try { if (!await entry.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."); } } catch (Exception) when (DropReferenceOnFailure(instanceUniqueName, entry)) { // DropReferenceOnFailure always returns false; the filter just runs // the cleanup so the reservation is not leaked when WaitAsync throws // or times out (TimeoutException / OperationCanceledException). The // exception still propagates. The semaphore was NOT entered on any // of these paths, so only the reference is dropped. throw; } return new LockRelease(this, instanceUniqueName, entry); } /// /// Checks whether a lock is currently held for the given instance (for diagnostics). /// /// The unique name of the instance to check. /// true if a lock is currently held; otherwise false. public bool IsLocked(string instanceUniqueName) { lock (_gate) { return _locks.TryGetValue(instanceUniqueName, out var entry) && entry.Semaphore.CurrentCount == 0; } } private bool DropReferenceOnFailure(string instanceUniqueName, LockEntry entry) { ReleaseReference(instanceUniqueName, entry, semaphoreWasEntered: false); return false; } /// /// Drops one reference to the entry. When /// is true the semaphore is released first. When the reference count reaches /// zero the entry is removed from the dictionary and the semaphore disposed. /// private void ReleaseReference(string instanceUniqueName, LockEntry entry, bool semaphoreWasEntered) { lock (_gate) { // Release the semaphore (handing the lock to any waiter) and drop the // reference under the same gate, so the dispose decision below cannot // race with the Release on an entry that another caller is reclaiming. if (semaphoreWasEntered) { entry.Semaphore.Release(); } entry.RefCount--; if (entry.RefCount <= 0 && _locks.TryGetValue(instanceUniqueName, out var current) && ReferenceEquals(current, entry)) { _locks.Remove(instanceUniqueName); entry.Semaphore.Dispose(); } } } private sealed class LockEntry { public readonly SemaphoreSlim Semaphore = new(1, 1); /// Number of in-flight acquires (waiters + the current holder). Guarded by . public int RefCount; } private sealed class LockRelease : IDisposable { private readonly OperationLockManager _owner; private readonly string _instanceUniqueName; private readonly LockEntry _entry; private int _disposed; /// Initializes the release handle with its owner, instance name, and lock entry. /// The owning . /// The instance unique name whose lock is held. /// The ref-counted semaphore entry to release on dispose. public LockRelease(OperationLockManager owner, string instanceUniqueName, LockEntry entry) { _owner = owner; _instanceUniqueName = instanceUniqueName; _entry = entry; } /// Releases the operation lock idempotently. public void Dispose() { if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) { _owner.ReleaseReference(_instanceUniqueName, _entry, semaphoreWasEntered: true); } } } }