fix(abcip): throttle evict-recreate Forward-Open attempts per device (05/STAB-8)
This commit is contained in:
@@ -455,6 +455,8 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
}
|
||||
await probeRuntime.ReadAsync(ct).ConfigureAwait(false);
|
||||
success = probeRuntime.GetStatus() == 0;
|
||||
if (success)
|
||||
state.Backoff.RecordSuccess(); // 05/STAB-8: device reachable — reset the data-path window
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
@@ -471,6 +473,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
try { probeRuntime?.Dispose(); } catch { }
|
||||
probeRuntime = null;
|
||||
state.ProbeInitialized = false;
|
||||
state.Backoff.RecordFailure(DateTime.UtcNow); // 05/STAB-8: keep the data-path window open
|
||||
}
|
||||
|
||||
TransitionDeviceState(state, success ? HostState.Running : HostState.Stopped);
|
||||
@@ -900,6 +903,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
return device.ParentRuntimes[parentTagName];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when a per-device <see cref="ConnectionBackoff"/> window is still open (05/STAB-8)
|
||||
/// — a fail-fast that costs no wire traffic. Read/write map it to <c>BadCommunicationError</c>
|
||||
/// via their generic catch, exactly as a real Forward-Open failure.
|
||||
/// </summary>
|
||||
private sealed class ConnectionThrottledException(string message) : Exception(message);
|
||||
|
||||
/// <summary>
|
||||
/// Idempotently materialise the runtime handle for a tag definition. First call creates
|
||||
/// + initialises the libplctag Tag; subsequent calls reuse the cached handle for the
|
||||
@@ -914,6 +924,14 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
?? throw new InvalidOperationException(
|
||||
$"AbCip tag '{def.Name}' has malformed TagPath '{def.TagPath}'.");
|
||||
|
||||
// 05/STAB-8: inside an open backoff window a data-path caller fails fast — no runtime
|
||||
// create, no Forward-Open. The malformed-path config error above still surfaces (a permanent
|
||||
// fault must never be masked by the window); only the expensive create+init is throttled. The
|
||||
// probe loop bypasses this (its own runtime) and drives the failure/success recording.
|
||||
if (!device.Backoff.ShouldAttempt(DateTime.UtcNow))
|
||||
throw new ConnectionThrottledException(
|
||||
$"AbCip runtime create for '{def.Name}' on {device.Options.HostAddress} throttled — backoff window open after a recent Forward-Open failure.");
|
||||
|
||||
// Review I-1 — an array tag (the EXPLICIT IsArray flag, incl. a 1-element array) sets
|
||||
// libplctag's elem_count so the read pulls every element in one CIP transaction; the read
|
||||
// path then boxes them into a typed CLR array. Scalar tags pass count 1 + IsArray false.
|
||||
@@ -927,8 +945,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
catch
|
||||
{
|
||||
runtime.Dispose();
|
||||
device.Backoff.RecordFailure(DateTime.UtcNow); // 05/STAB-8: open the backoff window
|
||||
throw;
|
||||
}
|
||||
device.Backoff.RecordSuccess(); // 05/STAB-8: Forward-Open succeeded — reset the window
|
||||
// Two concurrent callers can both miss the cache + both initialize a runtime; only the
|
||||
// first TryAdd wins. Dispose the loser so it doesn't leak a native tag handle.
|
||||
if (device.Runtimes.TryAdd(def.Name, runtime))
|
||||
@@ -1254,6 +1274,14 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
/// <summary>Gets the lock object used for probe synchronization.</summary>
|
||||
public object ProbeLock { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Per-device connect-attempt throttle (05/STAB-8). After a failed runtime create
|
||||
/// (Forward-Open), data-path callers fail fast inside the backoff window instead of
|
||||
/// re-creating + re-initializing a runtime per tag per tick; the probe loop bypasses it
|
||||
/// (its own runtime) and drives failure/success so a recovered device resets the window.
|
||||
/// </summary>
|
||||
public ConnectionBackoff Backoff { get; } = new(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(30));
|
||||
/// <summary>Gets or sets the current host state of this device.</summary>
|
||||
public HostState HostState { get; set; } = HostState.Unknown;
|
||||
/// <summary>Gets or sets the UTC timestamp when the host state was last changed.</summary>
|
||||
|
||||
Reference in New Issue
Block a user