a5a0f96d49
Follow-ups from the fleet-wide read-timeout audit that the S7 R2-01 read-leg fix (PR #453) prompted. The audit confirmed S7 was the ONLY driver with the async-read-ignores-socket-timeout hang; these are the two adjacent (non-hang) findings it surfaced. 1. FOCAS TimeoutMs:0 footgun (the risky one): a non-positive Timeout made SynchronizedFocasClient DISABLE its per-call wall-clock ceiling, reverting to the caller's long-lived poll token — reintroducing exactly the frozen-peer wedge S7 just eliminated, under misconfig. Clamp non-positive TimeoutMs to the 2s default at the config boundary so the deadline can never be authored away. 2. TimeoutMs validation symmetry: apply the same clamp in the AbCip + AbLegacy factories. libplctag's Tag.Timeout setter throws on <=0, faulting tag creation on every read/write; clamping keeps a misconfigured TimeoutMs:0 running on the default bound instead. (Shared PositiveTimeoutOrDefault helper per factory.) 3. AbLegacy reconnect parity with AbCip: AbLegacy evicted the cached libplctag runtime on neither the non-zero-status nor transport-exception read/write path (AbCip evicts on both), so a data-path fault recovered only via the probe loop / libplctag internals. Added EvictRuntime + wired it into both read and write failure paths so a fresh handle is created on the next call. Tests: FOCAS 265->269 (clamp theory + positive), AbCip 336->339 (clamp theory + positive), AbLegacy 209->212 (read-nonzero / read-exception / write-nonzero evict). No production regressions; all three driver suites green.
110 lines
4.8 KiB
C#
110 lines
4.8 KiB
C#
using libplctag;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
|
|
|
|
/// <summary>
|
|
/// Driver-timeout-hardening parity with AbCip: a read/write that fails with a non-zero libplctag
|
|
/// status or a transport exception must EVICT the cached runtime so the next call re-creates a
|
|
/// fresh handle — rather than returning the same failure forever until the probe loop happens to
|
|
/// recycle it. Previously AbLegacy evicted on neither path (unlike AbCip).
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class AbLegacyEvictOnFailureTests
|
|
{
|
|
private const string Device = "ab://10.0.0.5/1,0";
|
|
|
|
// Probe disabled — the probe loop also calls _tagFactory.Create, which would perturb the
|
|
// creation count these tests assert on.
|
|
private static (AbLegacyDriver drv, FakeAbLegacyTagFactory factory) NewDriver(params AbLegacyTagDefinition[] tags)
|
|
{
|
|
var factory = new FakeAbLegacyTagFactory();
|
|
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
|
|
{
|
|
Devices = [new AbLegacyDeviceOptions(Device)],
|
|
Tags = tags,
|
|
Probe = new AbLegacyProbeOptions { Enabled = false },
|
|
}, "drv-1", factory);
|
|
return (drv, factory);
|
|
}
|
|
|
|
/// <summary>A non-zero libplctag status on read evicts the runtime; the next read creates a fresh handle and recovers.</summary>
|
|
[Fact]
|
|
public async Task Read_nonzero_status_evicts_runtime_so_next_read_creates_fresh_handle()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("Counter", Device, "N7:0", AbLegacyDataType.Int));
|
|
var callCount = 0;
|
|
factory.Customise = p =>
|
|
{
|
|
callCount++;
|
|
return callCount == 1
|
|
? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection }
|
|
: new FakeAbLegacyTag(p) { Status = 0, Value = 42 };
|
|
};
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
// First read — bad status → runtime evicted, no reuse.
|
|
var first = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
|
first.Single().StatusCode.ShouldNotBe(AbLegacyStatusMapper.Good);
|
|
|
|
// Second read — fresh handle, succeeds.
|
|
var second = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
|
second.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
second.Single().Value.ShouldBe(42);
|
|
callCount.ShouldBe(2); // one failed handle + one fresh handle
|
|
}
|
|
|
|
/// <summary>A transport exception on read evicts the runtime; the next read creates a fresh handle and recovers.</summary>
|
|
[Fact]
|
|
public async Task Read_transport_exception_evicts_runtime_so_next_read_creates_fresh_handle()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("Counter", Device, "N7:0", AbLegacyDataType.Int));
|
|
var callCount = 0;
|
|
factory.Customise = p =>
|
|
{
|
|
callCount++;
|
|
return callCount == 1
|
|
? new FakeAbLegacyTag(p) { ThrowOnRead = true, Exception = new System.Net.Sockets.SocketException() }
|
|
: new FakeAbLegacyTag(p) { Status = 0, Value = 7 };
|
|
};
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var first = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
|
first.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.BadCommunicationError);
|
|
|
|
var second = await drv.ReadAsync(["Counter"], CancellationToken.None);
|
|
second.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
second.Single().Value.ShouldBe(7);
|
|
callCount.ShouldBe(2);
|
|
}
|
|
|
|
/// <summary>A non-zero libplctag status on write evicts the runtime; the next write creates a fresh handle and recovers.</summary>
|
|
[Fact]
|
|
public async Task Write_nonzero_status_evicts_runtime_so_next_write_creates_fresh_handle()
|
|
{
|
|
var (drv, factory) = NewDriver(
|
|
new AbLegacyTagDefinition("Counter", Device, "N7:0", AbLegacyDataType.Int));
|
|
var callCount = 0;
|
|
factory.Customise = p =>
|
|
{
|
|
callCount++;
|
|
return callCount == 1
|
|
? new FakeAbLegacyTag(p) { Status = (int)Status.ErrorBadConnection }
|
|
: new FakeAbLegacyTag(p) { Status = 0 };
|
|
};
|
|
await drv.InitializeAsync("{}", CancellationToken.None);
|
|
|
|
var first = await drv.WriteAsync([new WriteRequest("Counter", (short)5)], CancellationToken.None);
|
|
first.Single().StatusCode.ShouldNotBe(AbLegacyStatusMapper.Good);
|
|
|
|
var second = await drv.WriteAsync([new WriteRequest("Counter", (short)5)], CancellationToken.None);
|
|
second.Single().StatusCode.ShouldBe(AbLegacyStatusMapper.Good);
|
|
callCount.ShouldBe(2);
|
|
}
|
|
}
|