feat(dcl): add StaleTagMonitor for heartbeat-based disconnect detection
Composable StaleTagMonitor class in Commons fires a Stale event when no value is received within a configurable max silence period. Integrated into both LmxProxyDataConnection and OpcUaDataConnection adapters via optional HeartbeatTagPath/HeartbeatMaxSilence connection config keys. When stale, the adapter fires Disconnected triggering the standard reconnect cycle. 10 unit tests cover timer behavior.
This commit is contained in:
68
src/ScadaLink.Commons/Types/StaleTagMonitor.cs
Normal file
68
src/ScadaLink.Commons/Types/StaleTagMonitor.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
namespace ScadaLink.Commons.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Monitors a heartbeat tag subscription for staleness. If no value is received
|
||||
/// within <see cref="MaxSilence"/>, the <see cref="Stale"/> event fires.
|
||||
/// Composable into any IDataConnection adapter.
|
||||
/// </summary>
|
||||
public sealed class StaleTagMonitor : IDisposable
|
||||
{
|
||||
private readonly TimeSpan _maxSilence;
|
||||
private Timer? _timer;
|
||||
private volatile bool _staleFired;
|
||||
|
||||
public StaleTagMonitor(TimeSpan maxSilence)
|
||||
{
|
||||
if (maxSilence <= TimeSpan.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxSilence), "MaxSilence must be positive.");
|
||||
_maxSilence = maxSilence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires when no value has been received within <see cref="MaxSilence"/>.
|
||||
/// Fires once per stale period — resets after <see cref="OnValueReceived"/> is called.
|
||||
/// </summary>
|
||||
public event Action? Stale;
|
||||
|
||||
public TimeSpan MaxSilence => _maxSilence;
|
||||
|
||||
/// <summary>
|
||||
/// Start monitoring. The timer begins counting from now.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_staleFired = false;
|
||||
_timer?.Dispose();
|
||||
_timer = new Timer(OnTimerElapsed, null, _maxSilence, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signal that a value was received. Resets the stale timer.
|
||||
/// </summary>
|
||||
public void OnValueReceived()
|
||||
{
|
||||
_staleFired = false;
|
||||
_timer?.Change(_maxSilence, Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop monitoring and dispose the timer.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
private void OnTimerElapsed(object? state)
|
||||
{
|
||||
if (_staleFired) return;
|
||||
_staleFired = true;
|
||||
Stale?.Invoke();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user