fix(drivers): harden operator TimeoutMs handling + AbLegacy evict parity
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.
This commit is contained in:
@@ -57,6 +57,40 @@ public sealed class FocasFactoryConfigTests
|
||||
drv.Options.Devices[0].Series.ShouldBe(FocasCncSeries.Thirty_i);
|
||||
}
|
||||
|
||||
// ---- Driver timeout hardening: a non-positive TimeoutMs must not disable the per-call ceiling ----
|
||||
|
||||
/// <summary>
|
||||
/// A misconfigured <c>TimeoutMs: 0</c> (or negative) must clamp to the 2 s default, NOT map to
|
||||
/// <see cref="TimeSpan.Zero"/> — a zero/negative <c>Timeout</c> makes <c>SynchronizedFocasClient</c>
|
||||
/// disable its per-call wall-clock ceiling, reintroducing the unbounded-read-on-a-frozen-peer
|
||||
/// wedge the S7 R2-01 read-leg fix eliminated. Guards the factory clamp.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(-5000)]
|
||||
public void CreateInstance_clamps_non_positive_TimeoutMs_to_the_default(int timeoutMs)
|
||||
{
|
||||
var json = $$$"""{"Backend":"unimplemented","TimeoutMs":{{{timeoutMs}}},"Probe":{"TimeoutMs":{{{timeoutMs}}}}}""";
|
||||
|
||||
var drv = FocasDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||
|
||||
drv.Options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(2_000));
|
||||
drv.Options.Timeout.ShouldBeGreaterThan(TimeSpan.Zero);
|
||||
drv.Options.Probe.Timeout.ShouldBe(TimeSpan.FromMilliseconds(2_000));
|
||||
}
|
||||
|
||||
/// <summary>A positive <c>TimeoutMs</c> is honoured verbatim (the clamp only rewrites non-positive values).</summary>
|
||||
[Fact]
|
||||
public void CreateInstance_honours_a_positive_TimeoutMs()
|
||||
{
|
||||
const string json = """{"Backend":"unimplemented","TimeoutMs":750}""";
|
||||
|
||||
var drv = FocasDriverFactoryExtensions.CreateInstance("drv-1", json);
|
||||
|
||||
drv.Options.Timeout.ShouldBe(TimeSpan.FromMilliseconds(750));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the AlarmProjection configuration section is mapped to driver options.</summary>
|
||||
[Fact]
|
||||
public void CreateInstance_maps_AlarmProjection_section_onto_options()
|
||||
|
||||
Reference in New Issue
Block a user