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:
129
tests/ScadaLink.Commons.Tests/Types/StaleTagMonitorTests.cs
Normal file
129
tests/ScadaLink.Commons.Tests/Types/StaleTagMonitorTests.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using ScadaLink.Commons.Types;
|
||||
|
||||
namespace ScadaLink.Commons.Tests.Types;
|
||||
|
||||
public class StaleTagMonitorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ZeroTimeSpan_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StaleTagMonitor(TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeTimeSpan_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StaleTagMonitor(TimeSpan.FromSeconds(-1)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stale_FiresAfterMaxSilence()
|
||||
{
|
||||
using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(100));
|
||||
var staleCount = 0;
|
||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||
monitor.Start();
|
||||
|
||||
await Task.Delay(300);
|
||||
Assert.Equal(1, staleCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stale_FiresOnlyOnce()
|
||||
{
|
||||
using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(50));
|
||||
var staleCount = 0;
|
||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||
monitor.Start();
|
||||
|
||||
await Task.Delay(300);
|
||||
Assert.Equal(1, staleCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnValueReceived_ResetsTimer()
|
||||
{
|
||||
using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(200));
|
||||
var staleCount = 0;
|
||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||
monitor.Start();
|
||||
|
||||
// Keep resetting before the 200ms deadline
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
monitor.OnValueReceived();
|
||||
}
|
||||
|
||||
// Should not have gone stale
|
||||
Assert.Equal(0, staleCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnValueReceived_AllowsStaleAfterSilence()
|
||||
{
|
||||
using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(100));
|
||||
var staleCount = 0;
|
||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||
monitor.Start();
|
||||
|
||||
// Reset once
|
||||
await Task.Delay(50);
|
||||
monitor.OnValueReceived();
|
||||
|
||||
// Then go silent
|
||||
await Task.Delay(250);
|
||||
Assert.Equal(1, staleCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnValueReceived_ResetsStaleFlag_AllowsSecondFire()
|
||||
{
|
||||
using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(100));
|
||||
var staleCount = 0;
|
||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||
monitor.Start();
|
||||
|
||||
// Wait for first stale
|
||||
await Task.Delay(250);
|
||||
Assert.Equal(1, staleCount);
|
||||
|
||||
// Reset — should allow second stale fire
|
||||
monitor.OnValueReceived();
|
||||
await Task.Delay(250);
|
||||
Assert.Equal(2, staleCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stop_PreventsStale()
|
||||
{
|
||||
using var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(50));
|
||||
var staleCount = 0;
|
||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||
monitor.Start();
|
||||
monitor.Stop();
|
||||
|
||||
await Task.Delay(200);
|
||||
Assert.Equal(0, staleCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Dispose_PreventsStale()
|
||||
{
|
||||
var monitor = new StaleTagMonitor(TimeSpan.FromMilliseconds(50));
|
||||
var staleCount = 0;
|
||||
monitor.Stale += () => Interlocked.Increment(ref staleCount);
|
||||
monitor.Start();
|
||||
monitor.Dispose();
|
||||
|
||||
await Task.Delay(200);
|
||||
Assert.Equal(0, staleCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSilence_ReturnsConfiguredValue()
|
||||
{
|
||||
using var monitor = new StaleTagMonitor(TimeSpan.FromSeconds(42));
|
||||
Assert.Equal(TimeSpan.FromSeconds(42), monitor.MaxSilence);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user