From 4bb25fc03977ee23b346b2ea5d7e0e1ff0060d4f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 09:52:42 -0400 Subject: [PATCH] =?UTF-8?q?fix(r2-01):=20surface=20connect-timeout=20as=20?= =?UTF-8?q?TimeoutException,=20not=20OCE=20=E2=80=94=20poll=20loops=20no?= =?UTF-8?q?=20longer=20die=20on=20unreachable-host=20outages=20(STAB-14,?= =?UTF-8?q?=20task=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2-01-s7-fault-hardening-plan.md.tasks.json | 2 +- .../ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/archreview/plans/R2-01-s7-fault-hardening-plan.md.tasks.json b/archreview/plans/R2-01-s7-fault-hardening-plan.md.tasks.json index 066c703e..8136a897 100644 --- a/archreview/plans/R2-01-s7-fault-hardening-plan.md.tasks.json +++ b/archreview/plans/R2-01-s7-fault-hardening-plan.md.tasks.json @@ -22,7 +22,7 @@ { "id": 3, "subject": "GREEN: EnsureConnectedAsync + InitializeAsync connect-timeout OCE -> TimeoutException conversion (STAB-14 root fix)", - "status": "pending", + "status": "completed", "blockedBy": [1, 2] }, { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs index 282fecd1..31afceed 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs @@ -187,6 +187,16 @@ public sealed class S7Driver cts.CancelAfter(_options.Timeout); await plc.OpenAsync(cts.Token).ConfigureAwait(false); } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + // The linked CancelAfter fired — a connect TIMEOUT, not a caller cancellation. + // Surface it as a TimeoutException so init-time and reconnect-time timeouts report + // the same exception type (STAB-14 consistency); the outer catch still faults health. + try { plc.Dispose(); } catch { } + throw new TimeoutException( + $"S7 connect to {_options.Host}:{_options.Port} timed out after " + + $"{(int)_options.Timeout.TotalMilliseconds} ms."); + } catch { // Dispose the partially-constructed connection so a failed init doesn't leak it @@ -1205,6 +1215,17 @@ public sealed class S7Driver cts.CancelAfter(_options.Timeout); await plc.OpenAsync(cts.Token).ConfigureAwait(false); } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + // The linked CancelAfter fired — a connect TIMEOUT, not a caller cancellation. + // Surface it as a connection FAILURE (TimeoutException) so callers route it through + // their degrade paths: an escaping OCE reads as teardown and permanently killed the + // subscription poll loops (STAB-14, the unreachable-host regression in the Crit-3 fix). + try { plc.Dispose(); } catch { /* best-effort */ } + throw new TimeoutException( + $"S7 connect to {_options.Host}:{_options.Port} timed out after " + + $"{(int)_options.Timeout.TotalMilliseconds} ms."); + } catch { try { plc.Dispose(); } catch { /* best-effort */ }