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