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);
}
}
}
}