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:
@@ -264,6 +264,8 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
|
||||
if (status != 0)
|
||||
{
|
||||
// Evict the stale handle so the next call re-creates it (mirrors AbCip).
|
||||
EvictRuntime(device, def.Name);
|
||||
results[i] = new DataValueSnapshot(null,
|
||||
AbLegacyStatusMapper.MapLibplctagStatus(status), null, now);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead,
|
||||
@@ -287,6 +289,8 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Transport exception — evict so the next read creates a fresh handle (mirrors AbCip).
|
||||
EvictRuntime(device, def.Name);
|
||||
results[i] = new DataValueSnapshot(null,
|
||||
AbLegacyStatusMapper.BadCommunicationError, null, now);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
|
||||
@@ -360,9 +364,16 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
opLock.Release();
|
||||
}
|
||||
|
||||
results[i] = new WriteResult(status == 0
|
||||
? AbLegacyStatusMapper.Good
|
||||
: AbLegacyStatusMapper.MapLibplctagStatus(status));
|
||||
if (status != 0)
|
||||
{
|
||||
// Evict the stale handle so the next call re-creates it (mirrors AbCip).
|
||||
EvictRuntime(device, def.Name);
|
||||
results[i] = new WriteResult(AbLegacyStatusMapper.MapLibplctagStatus(status));
|
||||
}
|
||||
else
|
||||
{
|
||||
results[i] = new WriteResult(AbLegacyStatusMapper.Good);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (NotSupportedException nse)
|
||||
@@ -380,6 +391,8 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Transport exception — evict so the next write/read creates a fresh handle (mirrors AbCip).
|
||||
EvictRuntime(device, def.Name);
|
||||
results[i] = new WriteResult(AbLegacyStatusMapper.BadCommunicationError);
|
||||
_health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message);
|
||||
}
|
||||
@@ -685,6 +698,26 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evict the runtime for <paramref name="tagName"/> from the device's cache and dispose it so
|
||||
/// the next read/write call re-creates and re-initializes a fresh libplctag handle. Called
|
||||
/// after a non-zero libplctag status or a transport exception on a read/write — a non-zero
|
||||
/// status can mean the controller dropped the connection or the handle became permanently
|
||||
/// invalid (e.g. after a PLC download), and reusing it would return the same failure forever.
|
||||
/// Mirrors <c>AbCipDriver.EvictRuntime</c> and the probe loop's recreate-on-failure behaviour
|
||||
/// (previously AbLegacy leaned only on the probe loop + libplctag's internal recovery, so a
|
||||
/// data-path fault recovered more slowly than in AbCip).
|
||||
/// </summary>
|
||||
/// <param name="device">The device whose runtime cache holds the stale handle.</param>
|
||||
/// <param name="tagName">The tag name key of the runtime to evict.</param>
|
||||
private static void EvictRuntime(DeviceState device, string tagName)
|
||||
{
|
||||
if (device.Runtimes.TryRemove(tagName, out var stale))
|
||||
{
|
||||
try { stale.Dispose(); } catch { /* best-effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronous teardown. Mirrors the body of
|
||||
/// <see cref="ShutdownAsync"/> but never wraps the async path in
|
||||
|
||||
Reference in New Issue
Block a user