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:
@@ -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&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&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&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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user