refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
|
||||
|
||||
/// <summary>
|
||||
/// 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 +
|
||||
/// <see cref="IDisposable.Dispose"/>d when the last reference is released — so the dictionary
|
||||
/// does not accumulate one kernel wait handle per distinct instance name forever.
|
||||
/// </summary>
|
||||
public class OperationLockManager
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<string, LockEntry> _locks = new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Number of lock entries currently tracked. Used for diagnostics and to
|
||||
/// verify that semaphores are reclaimed (DeploymentManager-005).
|
||||
/// </summary>
|
||||
public int TrackedLockCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _locks.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="instanceUniqueName">The unique name of the instance to lock.</param>
|
||||
/// <param name="timeout">Maximum time to wait for the lock before throwing <see cref="TimeoutException"/>.</param>
|
||||
/// <param name="cancellationToken">Cancellation token to abort the wait.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> that releases the lock when disposed.</returns>
|
||||
public async Task<IDisposable> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a lock is currently held for the given instance (for diagnostics).
|
||||
/// </summary>
|
||||
/// <param name="instanceUniqueName">The unique name of the instance to check.</param>
|
||||
/// <returns><c>true</c> if a lock is currently held; otherwise <c>false</c>.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops one reference to the entry. When <paramref name="semaphoreWasEntered"/>
|
||||
/// is true the semaphore is released first. When the reference count reaches
|
||||
/// zero the entry is removed from the dictionary and the semaphore disposed.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>Number of in-flight acquires (waiters + the current holder). Guarded by <see cref="_gate"/>.</summary>
|
||||
public int RefCount;
|
||||
}
|
||||
|
||||
private sealed class LockRelease : IDisposable
|
||||
{
|
||||
private readonly OperationLockManager _owner;
|
||||
private readonly string _instanceUniqueName;
|
||||
private readonly LockEntry _entry;
|
||||
private int _disposed;
|
||||
|
||||
/// <summary>Initializes the release handle with its owner, instance name, and lock entry.</summary>
|
||||
/// <param name="owner">The owning <see cref="OperationLockManager"/>.</param>
|
||||
/// <param name="instanceUniqueName">The instance unique name whose lock is held.</param>
|
||||
/// <param name="entry">The ref-counted semaphore entry to release on dispose.</param>
|
||||
public LockRelease(OperationLockManager owner, string instanceUniqueName, LockEntry entry)
|
||||
{
|
||||
_owner = owner;
|
||||
_instanceUniqueName = instanceUniqueName;
|
||||
_entry = entry;
|
||||
}
|
||||
|
||||
/// <summary>Releases the operation lock idempotently.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0)
|
||||
{
|
||||
_owner.ReleaseReference(_instanceUniqueName, _entry, semaphoreWasEntered: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user