e5527d0201
Review 07 P7 (High): reads walked every reference of every device serially with no backoff, so a dead device cost Timeout per tag per poll (50 tags x 2 s = 100 s) and starved every healthy device long enough to trip the comms-loss watchdog against a reachable one. - ReadAsync buckets references by device and reads up to 8 devices concurrently; each device's tags are still read in order. - Each device has a ConnectionBackoff (1 s doubling to 30 s). Two consecutive comms faults mark it down: the rest of that pass and every later pass fail fast with BadCommunicationError and touch no wire until the window expires or any answer (data path or probe) resets it. One timing-out tag does not blacken its answering siblings. - Cold connects are single-flight per device (ConnectGate), replacing the per-key creation locks; a cached handle never touches the gate. - A status/exception proving the device ANSWERED with a deterministic refusal (not found, not allowed, out of bounds, type/size mismatch) is per-tag Bad, keeps its handle, warns once per tag and advances LastSuccessfulRead instead of evicting and degrading. Writes classify the same way. - Health is written once per read: Degraded if any device faulted, Healthy if anything read Good, LastSuccessfulRead advanced when any device answered. Adds an env-gated ab_server case with a blackholed sibling device. Refs #738 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
66 lines
3.3 KiB
C#
66 lines
3.3 KiB
C#
using System.Diagnostics;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Review 07 P7 (High), wire level: one driver serving a live <c>ab_server</c> device AND a dead device (a
|
|
/// blackholed address, so every connect waits out the full timeout). Before the fix the dead device's 20 tags
|
|
/// cost 20 x Timeout per poll and the live device was read once per cycle; now the live device reads Good in
|
|
/// the same call, the dead device costs at most two timeouts, and the next poll does not touch it at all.
|
|
/// Read-only.
|
|
/// </summary>
|
|
[Collection(AbLegacyServerCollection.Name)]
|
|
[Trait("Category", "Integration")]
|
|
[Trait("Simulator", "ab_server-PCCC")]
|
|
public sealed class AbLegacyDeadDeviceIsolationTests(AbLegacyServerFixture sim)
|
|
{
|
|
// TEST-NET-1 (RFC 5737): never routed, so a connect blackholes until the driver's timeout.
|
|
private const string DeadUri = "ab://192.0.2.1:44818/1,0";
|
|
|
|
/// <summary>The live device is served promptly while a blackholed sibling is throttled.</summary>
|
|
/// <param name="profile">The simulator profile.</param>
|
|
/// <param name="transport">The transport stack the driver runs on.</param>
|
|
[AbLegacyTheory]
|
|
[MemberData(nameof(TransportMatrix.Profiles), MemberType = typeof(TransportMatrix))]
|
|
public async Task Dead_device_does_not_stall_the_live_one(AbLegacyServerProfile profile, AbLegacyTransport transport)
|
|
{
|
|
if (sim.SkipReasonFor(profile) is { } skip) Assert.Skip(skip);
|
|
|
|
var liveUri = $"ab://{sim.Host}:{sim.PortFor(profile)}/{sim.CipPath}";
|
|
var timeout = TimeSpan.FromSeconds(2);
|
|
var dead = Enumerable.Range(0, 20)
|
|
.Select(i => new AbLegacyTagDefinition($"Dead{i}", DeadUri, $"N7:{i}", AbLegacyDataType.Int))
|
|
.ToArray();
|
|
await using var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions(liveUri, profile.Family), new AbLegacyDeviceOptions(DeadUri, profile.Family)],
|
|
RawTags = AbLegacyRawTags.Entries(
|
|
[new AbLegacyTagDefinition("Live", liveUri, "N7:0", AbLegacyDataType.Int), .. dead]),
|
|
Timeout = timeout,
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
Transport = transport,
|
|
}, driverInstanceId: $"ablegacy-p7-{profile.Family}-{transport}");
|
|
var ct = TestContext.Current.CancellationToken;
|
|
await drv.InitializeAsync("{}", ct);
|
|
string[] refs = ["Live", .. dead.Select(d => d.Name)];
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
var first = await drv.ReadAsync(refs, ct);
|
|
sw.Stop();
|
|
|
|
first[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good, "the live device must be served in the same poll");
|
|
first.Skip(1).ShouldAllBe(s => s.StatusCode != AbLegacyStatusMapper.Good);
|
|
sw.Elapsed.ShouldBeLessThan(timeout * 3, "a dead device may cost at most two timeouts per backoff window");
|
|
|
|
sw.Restart();
|
|
var second = await drv.ReadAsync(refs, ct);
|
|
sw.Stop();
|
|
second[0].StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
sw.Elapsed.ShouldBeLessThan(timeout, "inside the backoff window the dead device is not dialled");
|
|
drv.GetHealth().State.ShouldBe(DriverState.Degraded);
|
|
}
|
|
}
|