diff --git a/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ErrorClassifier.cs b/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ErrorClassifier.cs index be1509e0..de21b912 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ErrorClassifier.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ErrorClassifier.cs @@ -26,6 +26,10 @@ public static class ErrorClassifier /// /// Determines whether an exception represents a transient failure. + /// Prefer the cancellation-aware + /// overload at call sites that hold the caller's token — this token-less overload + /// classifies every as transient, including + /// caller-requested cancellations that must never be buffered for S&F retry. /// /// The exception to classify. /// for connection/timeout/cancellation exceptions; otherwise. @@ -37,6 +41,18 @@ public static class ErrorClassifier or OperationCanceledException; } + /// + /// Cancellation-aware overload: a caller-requested cancellation is NOT transient + /// (buffering caller-abandoned work into S&F would be wrong). Prefer this overload + /// at call sites that hold the caller's token. + /// + public static bool IsTransient(Exception exception, CancellationToken cancellationToken) + { + if (exception is OperationCanceledException && cancellationToken.IsCancellationRequested) + return false; + return IsTransient(exception); + } + /// /// Creates a TransientException for S&F buffering. /// diff --git a/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs b/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs index 6904fb9b..9144b0e7 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/ExternalSystemClient.cs @@ -340,7 +340,7 @@ public class ExternalSystemClient : IExternalSystemClient throw ErrorClassifier.AsTransient( $"Timeout calling {system.Name} after {effectiveTimeout.TotalSeconds:0.##}s", ex); } - catch (Exception ex) when (ErrorClassifier.IsTransient(ex)) + catch (Exception ex) when (ErrorClassifier.IsTransient(ex, cancellationToken)) { throw ErrorClassifier.AsTransient($"Connection error to {system.Name}: {ex.Message}", ex); } diff --git a/tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/ErrorClassifierTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/ErrorClassifierTests.cs index d61ce571..7a548c6e 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/ErrorClassifierTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.Tests/ErrorClassifierTests.cs @@ -64,4 +64,13 @@ public class ErrorClassifierTests Assert.IsType(ex); Assert.Equal("test message", ex.Message); } + + [Fact] + public void IsTransient_CallerCancelledOce_IsNotTransient() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + Assert.False(ErrorClassifier.IsTransient(new OperationCanceledException(), cts.Token)); + Assert.True(ErrorClassifier.IsTransient(new OperationCanceledException(), CancellationToken.None)); + } }