fix(esg): cancellation-aware ErrorClassifier.IsTransient overload — caller-cancelled work can never classify transient

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 04:32:51 -04:00
parent 0ee9975111
commit 37cf7c1a44
3 changed files with 26 additions and 1 deletions
@@ -26,6 +26,10 @@ public static class ErrorClassifier
/// <summary>
/// Determines whether an exception represents a transient failure.
/// Prefer the cancellation-aware <see cref="IsTransient(Exception, CancellationToken)"/>
/// overload at call sites that hold the caller's token — this token-less overload
/// classifies every <see cref="OperationCanceledException"/> as transient, including
/// caller-requested cancellations that must never be buffered for S&amp;F retry.
/// </summary>
/// <param name="exception">The exception to classify.</param>
/// <returns><see langword="true"/> for connection/timeout/cancellation exceptions; <see langword="false"/> otherwise.</returns>
@@ -37,6 +41,18 @@ public static class ErrorClassifier
or OperationCanceledException;
}
/// <summary>
/// Cancellation-aware overload: a caller-requested cancellation is NOT transient
/// (buffering caller-abandoned work into S&amp;F would be wrong). Prefer this overload
/// at call sites that hold the caller's token.
/// </summary>
public static bool IsTransient(Exception exception, CancellationToken cancellationToken)
{
if (exception is OperationCanceledException && cancellationToken.IsCancellationRequested)
return false;
return IsTransient(exception);
}
/// <summary>
/// Creates a TransientException for S&amp;F buffering.
/// </summary>
@@ -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);
}
@@ -64,4 +64,13 @@ public class ErrorClassifierTests
Assert.IsType<TransientExternalSystemException>(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));
}
}