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