fix(r2-01): filter OCE catches on the owning token in read/write wrappers + poll loop (STAB-14 defense-in-depth, task 4)

This commit is contained in:
Joseph Doherty
2026-07-13 09:54:03 -04:00
parent 4bb25fc039
commit 963543a578
2 changed files with 17 additions and 6 deletions
@@ -495,7 +495,10 @@ public sealed class S7Driver
{
plc = await EnsureConnectedAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) { throw; }
// Rethrow ONLY caller cancellation. A timeout-born OCE (none expected after the
// EnsureConnectedAsync conversion, but e.g. an S7.Net-internal CTS) must degrade the
// batch below, not escape as teardown (STAB-14).
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
catch (Exception ex)
{
for (var i = 0; i < fullReferences.Count; i++)
@@ -1007,7 +1010,9 @@ public sealed class S7Driver
{
plc = await EnsureConnectedAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) { throw; }
// Rethrow ONLY caller cancellation — a timeout-born OCE degrades the batch below rather
// than escaping as teardown (STAB-14 defense-in-depth, mirrors ReadAsync).
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
catch (Exception ex)
{
for (var i = 0; i < writes.Count; i++)
@@ -1401,7 +1406,10 @@ public sealed class S7Driver
await PollOnceAsync(state, forceRaise: true, ct).ConfigureAwait(false);
consecutiveFailures = 0;
}
catch (OperationCanceledException) { return; }
// Only a teardown OCE (the loop's own token) exits the loop; any other OCE (e.g. a
// connect-timeout that slipped past the wrapper filter) falls to the backoff path below
// so the loop lives (STAB-14 defense-in-depth).
catch (OperationCanceledException) when (ct.IsCancellationRequested) { return; }
catch (Exception ex)
{
// First-read error — polling continues; log so the operator has an event trail.
@@ -1415,14 +1423,17 @@ public sealed class S7Driver
// ticks reset consecutiveFailures back to 0 so the cadence snaps back to Interval.
var delay = ComputeBackoffDelay(state.Interval, consecutiveFailures);
try { await Task.Delay(delay, ct).ConfigureAwait(false); }
catch (OperationCanceledException) { return; }
// Task.Delay can only observe ct-triggered OCE, so this filter is purely for uniformity.
catch (OperationCanceledException) when (ct.IsCancellationRequested) { return; }
try
{
await PollOnceAsync(state, forceRaise: false, ct).ConfigureAwait(false);
consecutiveFailures = 0;
}
catch (OperationCanceledException) { return; }
// Only teardown exits; any non-teardown OCE (e.g. a slipped-through connect timeout)
// backs off instead of killing the loop (STAB-14 defense-in-depth).
catch (OperationCanceledException) when (ct.IsCancellationRequested) { return; }
catch (Exception ex)
{
// Sustained polling error — loop continues with backoff; log + update health.