perf(s7): fail the rest of a pass fast once the handle is dead (R07:P1)
No read or write loop checked _plcDead between items: on a frozen-but- established peer the first op blew its deadline and marked the handle dead, and every remaining span / tag / write still went to the wire and could wait its own deadline under _gate (a 20-span pass ~100 s), with Degraded and the comms-loss watchdog's clock deferred until the pass ended. - The span loop, the multi-var chunk loop, the per-tag loop and the write loop stamp BadCommunicationError without I/O once _plcDead is set. - A multi-var chunk that dies on a transport loss (socket / IO / timeout / ConnectionError) marks the handle dead instead of falling back to one doomed per-tag read per member. The framing class is deliberately excluded there: snap7/S7.Net report a multi-var with one refused item as ReadData(WrongNumberReceivedBytes) over a healthy stream. - Live gate via an in-test freezable TCP proxy (counts request frames, not seconds). Mutation shows S7.Net 0.20 already self-closes on a cancelled op, so the gate pins the end-to-end property; the driver guard itself is proven by the unit tests (an op that ignores its token keeps the socket open). Refs #742 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -762,6 +762,13 @@ public sealed class S7Driver
|
||||
perTag.Add(span.Members[0].OriginalIndex);
|
||||
continue;
|
||||
}
|
||||
// Fail fast once an earlier span marked the handle dead (R07:P1): every
|
||||
// further wire op on it would pay the full deadline under the gate.
|
||||
if (_plcDead)
|
||||
{
|
||||
foreach (var member in span.Members) results[member.OriginalIndex] = DeadHandleSnapshot(now);
|
||||
continue;
|
||||
}
|
||||
await ReadSpanAsync(plc, span, results, now, outcome, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
perTag.AddRange(plan.LeftoverIndexes);
|
||||
@@ -790,6 +797,12 @@ public sealed class S7Driver
|
||||
|
||||
foreach (var i in perTag)
|
||||
{
|
||||
// Fail fast once the handle is dead (R07:P1) — see the span loop.
|
||||
if (_plcDead)
|
||||
{
|
||||
results[i] = DeadHandleSnapshot(now);
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
var value = await ReadOneAsync(plc, resolved[i]!, cancellationToken).ConfigureAwait(false);
|
||||
@@ -1069,6 +1082,13 @@ public sealed class S7Driver
|
||||
{
|
||||
var chunk = batchable.GetRange(start, Math.Min(MultiVarReadMaxItems, batchable.Count - start));
|
||||
|
||||
// Fail fast once the handle is dead (R07:P1) — see ReadAsync's span loop.
|
||||
if (_plcDead)
|
||||
{
|
||||
foreach (var (index, _, _) in chunk) results[index] = DeadHandleSnapshot(now);
|
||||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<S7DataItem> answered;
|
||||
try
|
||||
{
|
||||
@@ -1094,6 +1114,17 @@ public sealed class S7Driver
|
||||
_plcDead = true;
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (IsS7TransportLoss(ex))
|
||||
{
|
||||
// A transport loss is the connection's fault, not the batch's: falling back would
|
||||
// send one more doomed per-tag read per member, each paying the full deadline
|
||||
// (R07:P1). Mark the handle dead and fail the chunk (and, via the guards, the rest
|
||||
// of the pass) without I/O.
|
||||
_plcDead = true;
|
||||
outcome.RecordFailure(DriverState.Degraded, ex.Message);
|
||||
foreach (var (index, _, _) in chunk) results[index] = DeadHandleSnapshot(now);
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex,
|
||||
@@ -1974,6 +2005,13 @@ public sealed class S7Driver
|
||||
results[i] = new WriteResult(StatusBadNotWritable);
|
||||
continue;
|
||||
}
|
||||
// Fail the rest of the batch fast once a write marked the handle dead (R07:P1): the
|
||||
// next call reopens; this batch must not serialise one deadline per remaining write.
|
||||
if (_plcDead)
|
||||
{
|
||||
results[i] = new WriteResult(StatusBadCommunicationError);
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
await WriteOneAsync(plc, tag, w.Value, cancellationToken).ConfigureAwait(false);
|
||||
@@ -2415,6 +2453,37 @@ public sealed class S7Driver
|
||||
if (IsS7ConnectionFatal(ex)) _plcDead = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The result stamped, without any I/O, on an item a pass skipped because an earlier item had
|
||||
/// already marked the connection dead (R07:P1). The pass's health already carries the failure
|
||||
/// that marked it.
|
||||
/// </summary>
|
||||
/// <param name="now">The pass timestamp.</param>
|
||||
/// <returns>A <c>BadCommunicationError</c> snapshot.</returns>
|
||||
private static DataValueSnapshot DeadHandleSnapshot(DateTime now) => new(null, StatusBadCommunicationError, null, now);
|
||||
|
||||
/// <summary>
|
||||
/// The TRANSPORT subset of <see cref="IsS7ConnectionFatal"/>: a socket / IO / disposed handle /
|
||||
/// blown deadline / <see cref="ErrorCode.ConnectionError"/> anywhere in the chain. Excludes the
|
||||
/// framing class, because S7.Net reports a multi-var request with one refused item as
|
||||
/// <c>PlcException(ReadData, PlcException(WrongNumberReceivedBytes))</c> (live-probed on the snap7
|
||||
/// fixture) over a perfectly healthy stream — the multi-var path falls back per tag for that
|
||||
/// shape, and a genuine desync still surfaces on the per-tag reads.
|
||||
/// </summary>
|
||||
/// <param name="ex">The exception to classify.</param>
|
||||
/// <returns><see langword="true"/> for a transport loss.</returns>
|
||||
private static bool IsS7TransportLoss(Exception ex)
|
||||
{
|
||||
for (Exception? e = ex; e is not null; e = e.InnerException)
|
||||
{
|
||||
if (e is System.Net.Sockets.SocketException or System.IO.IOException or ObjectDisposedException or TimeoutException)
|
||||
return true;
|
||||
if (e is PlcException { ErrorCode: ErrorCode.ConnectionError })
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="ex"/> (or any inner exception) is a socket-level / connection
|
||||
/// loss OR an ISO-on-TCP framing/desync fault that a reopen can repair — a
|
||||
|
||||
Reference in New Issue
Block a user