using System.Collections.Concurrent; namespace ZB.MOM.WW.OtOpcUa.Server.Redundancy; /// /// Tracks in-progress publish-generation apply leases keyed on /// (ConfigGenerationId, PublishRequestId). Per decision #162 a sealed lease pattern /// ensures reflects every exit path (success / exception / /// cancellation) because the IAsyncDisposable returned by /// decrements unconditionally. /// /// /// A watchdog loop calls periodically with the configured /// ; any lease older than that is force-closed so a crashed /// publisher can't pin the node at . /// public sealed class ApplyLeaseRegistry { private readonly ConcurrentDictionary _leases = new(); private readonly TimeProvider _timeProvider; public TimeSpan ApplyMaxDuration { get; } public ApplyLeaseRegistry(TimeSpan? applyMaxDuration = null, TimeProvider? timeProvider = null) { ApplyMaxDuration = applyMaxDuration ?? TimeSpan.FromMinutes(10); _timeProvider = timeProvider ?? TimeProvider.System; } /// /// Register a new lease. Returns an whose disposal /// decrements the registry; use await using in the caller so every exit path /// closes the lease. /// public IAsyncDisposable BeginApplyLease(long generationId, Guid publishRequestId) { var key = new LeaseKey(generationId, publishRequestId); _leases[key] = _timeProvider.GetUtcNow().UtcDateTime; return new LeaseScope(this, key); } /// True when at least one apply lease is currently open. public bool IsApplyInProgress => !_leases.IsEmpty; /// Current open-lease count — diagnostics only. public int OpenLeaseCount => _leases.Count; /// Force-close any lease older than . Watchdog tick. /// Number of leases the watchdog closed on this tick. public int PruneStale() { var now = _timeProvider.GetUtcNow().UtcDateTime; var closed = 0; foreach (var kv in _leases) { if (now - kv.Value > ApplyMaxDuration && _leases.TryRemove(kv.Key, out _)) closed++; } return closed; } private void Release(LeaseKey key) => _leases.TryRemove(key, out _); private readonly record struct LeaseKey(long GenerationId, Guid PublishRequestId); private sealed class LeaseScope : IAsyncDisposable { private readonly ApplyLeaseRegistry _owner; private readonly LeaseKey _key; private int _disposed; public LeaseScope(ApplyLeaseRegistry owner, LeaseKey key) { _owner = owner; _key = key; } public ValueTask DisposeAsync() { if (Interlocked.Exchange(ref _disposed, 1) == 0) _owner.Release(_key); return ValueTask.CompletedTask; } } }