1

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))
    };
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
nop
  • 4,711
  • 6
  • 32
  • 93
  • Hi @nop, What is your question? – Peter Csala Oct 14 '22 at 08:40
  • 1
    @PeterCsala, hi! I want to expand the overloads of `DoAsync` to allow me specify an exception and `IsWebSocketErrorRetryEligible` in its action, basically to customize `.Handle` – nop Oct 14 '22 at 10:37

1 Answers1

1

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);
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • 1
    Thank you very much! :) I wonder if it's better to lift `` and add an additional overload or smt `public static async Task DoAsync(Func action, TimeSpan retryInterval, int retryCount = 3) { await DoAsync(async () => { await action(); return null; }, retryInterval, retryCount); }` – nop Oct 14 '22 at 12:03
  • 1
    @nop If you need to allow async methods as well, not just async functions then yes it does the trick. The only thing I would add a discard operator to make your intent explicit: `{ _ = await DoAsync(...` – Peter Csala Oct 14 '22 at 12:15