Home > Software design >  Polly Retry - Accept optional custom exception handler
Polly Retry - Accept optional custom exception handler

Time:10-15

How do I make the following Polly Retry policy let the user specify a custom handler such as the one below:

.Handle<WebSocketException>(exception => IsWebSocketErrorRetryEligible(exception))

Snippet

public static async Task DoAsync(Func<Task> action, TimeSpan retryInterval, int retryCount = 3)
{
    await DoAsync<object?>(async () =>
    {
        await action();
        return null;
    }, retryInterval, retryCount);
}

public static async Task<T> DoAsync<T>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 0)
{
    var policyResult = await Policy
        .Handle<Exception>()
        .WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
        .ExecuteAndCaptureAsync(action);

    if (policyResult.Outcome == OutcomeType.Failure)
    {
        throw policyResult.FinalException;
    }

    return policyResult.Result;
}

private bool IsWebSocketErrorRetryEligible(WebSocketException wex)
{
    if (wex.InnerException is HttpRequestException)
    {
        // assume transient failure
        return true;
    }

    return wex.WebSocketErrorCode switch
    {
        WebSocketError.ConnectionClosedPrematurely => true, // maybe a network blip?
        WebSocketError.Faulted => true, // maybe a server error or cosmic radiation?
        WebSocketError.HeaderError => true, // maybe a transient server error?
        WebSocketError.InvalidMessageType => false,
        WebSocketError.InvalidState => false,
        WebSocketError.NativeError => true, // maybe a transient server error?
        WebSocketError.NotAWebSocket => Regex.IsMatch(wex.Message, "\\b(5\\d\\d|408)\\b"), // 5xx errors   timeouts
        WebSocketError.Success => true, // should never happen, but try again
        WebSocketError.UnsupportedProtocol => false,
        WebSocketError.UnsupportedVersion => false,
        _ => throw new ArgumentOutOfRangeException(nameof(wex))
    };
}

CodePudding user response:

If you want to allow the consumer of your DoAsync method do define a custom predicate for the retry policy then you can do that like this:

public static async Task<T> DoAsync<T, TEx>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 0, Func<TEx, bool> exceptionFilter) where TEx: Exception
{
    var policyResult = await Policy
        .Handle<TEx>(exceptionFilter)
        .WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
        .ExecuteAndCaptureAsync(action);
    
    if (policyResult.Outcome == OutcomeType.Failure)
    {
        throw policyResult.FinalException;
    }

    return policyResult.Result;
}
  • Added TEx as a new generic type parameter and constrained it as Exception
  • Added a new Func<TEx, bool> parameter to the method
  • Replaced your catch-all-exception trigger (.Handle<Exception>()) to the user provided one

Note #1

If you need to allow to your consumers to define any number of exception filters then this technique can't be used because the TEx is part of the signature.

Note #2

There is no need to use ExecuteAndCaptureAsync and then branch based on the Outcome because you just re-implemented how the ExecuteAsync works.

public static async Task<T> DoAsync<T, TEx>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 0, Func<TEx, bool> exceptionFilter) where TEx: Exception
   => await Policy
        .Handle<TEx>(exceptionFilter)
        .WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
        .ExecuteAsync(action);
  • Related