91 lines
3.8 KiB
C#
91 lines
3.8 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Tests;
|
|
|
|
/// <summary>
|
|
/// 05/STAB-8 — a dead TwinCAT device must not be hammered with a full connect attempt on every
|
|
/// data call. The per-device <see cref="ConnectionBackoff"/> gates the connect/create path;
|
|
/// the probe loop bypasses it (its own interval is the recovery cadence) and a successful
|
|
/// connect resets the window instantly so recovery is never delayed. No TC3 fixture exists —
|
|
/// the fake-client suite is authoritative.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class TwinCATConnectBackoffTests
|
|
{
|
|
private const string Host = "ads://5.23.91.23.1.1:851";
|
|
|
|
private static TwinCATDriverOptions Options(bool probeEnabled) => new()
|
|
{
|
|
Devices = [new TwinCATDeviceOptions(Host)],
|
|
Tags = [new TwinCATTagDefinition("T", Host, "MAIN.T", TwinCATDataType.DInt)],
|
|
Probe = new TwinCATProbeOptions
|
|
{
|
|
Enabled = probeEnabled,
|
|
Interval = TimeSpan.FromMilliseconds(50),
|
|
Timeout = TimeSpan.FromMilliseconds(250),
|
|
},
|
|
EnableControllerBrowse = false,
|
|
};
|
|
|
|
/// <summary>Rapid data calls against a dead device attempt a single connect, then fail fast inside the backoff window.</summary>
|
|
[Fact]
|
|
public async Task DeadDevice_ConnectAttemptsFollowBackoffSchedule()
|
|
{
|
|
var factory = new FakeTwinCATClientFactory
|
|
{
|
|
Customise = () => new FakeTwinCATClient
|
|
{
|
|
ThrowOnConnect = true,
|
|
Exception = new InvalidOperationException("device down"),
|
|
},
|
|
};
|
|
var drv = new TwinCATDriver(Options(probeEnabled: false), "twincat-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
for (var i = 0; i < 6; i++)
|
|
await drv.ReadAsync(["T"], CancellationToken.None);
|
|
|
|
// One real connect attempt; the remaining five fail fast inside the 1 s window — no new
|
|
// client is created (the fake factory records one client per attempted connect).
|
|
factory.Clients.Count.ShouldBe(1);
|
|
}
|
|
|
|
/// <summary>The probe bypasses the backoff window and a successful connect resets it so the next data call succeeds immediately.</summary>
|
|
[Fact]
|
|
public async Task ProbeBypassesBackoff_AndSuccessResets()
|
|
{
|
|
var fail = true;
|
|
var factory = new FakeTwinCATClientFactory
|
|
{
|
|
Customise = () => new FakeTwinCATClient
|
|
{
|
|
ThrowOnConnect = fail,
|
|
Exception = new InvalidOperationException("device down"),
|
|
},
|
|
};
|
|
var drv = new TwinCATDriver(Options(probeEnabled: true), "twincat-1", factory);
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
// First data call opens the backoff window on a dead device.
|
|
var down = await drv.ReadAsync(["T"], CancellationToken.None);
|
|
down[0].StatusCode.ShouldBe(TwinCATStatusMapper.BadCommunicationError);
|
|
|
|
// Device recovers. The data path is still inside the window, but the probe bypasses it and
|
|
// reconnects — resetting the window so the following data read succeeds with no residual delay.
|
|
fail = false;
|
|
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(3);
|
|
DataValueSnapshot? good = null;
|
|
while (DateTime.UtcNow < deadline)
|
|
{
|
|
var r = await drv.ReadAsync(["T"], CancellationToken.None);
|
|
if (r[0].StatusCode == TwinCATStatusMapper.Good) { good = r[0]; break; }
|
|
await Task.Delay(25, CancellationToken.None);
|
|
}
|
|
|
|
good.ShouldNotBeNull("probe-driven reconnect must reset the backoff so data reads recover");
|
|
}
|
|
}
|