Ace
Ace

Reputation: 938

Using Polly for a retry attempt from an async function

I'm trying to retry a failed operation 3 times.

I'm using Polly for a retry operation.

I want to get the exception in case the retry operation fails and retry again 2 times and so on.

return await Policy
    .Handle<CustomException>()
    .RetryAsync(3, onRetryAsync: async (exception, retryCount, context) =>
    {
        return await runner.run(params);
    });

The function should return

Task<IReadOnlyCollection<string>>

I'm getting the following error:

async lambda expression converted to a task returning delegate cannot return a value

Upvotes: 22

Views: 41975

Answers (2)

Peter Csala
Peter Csala

Reputation: 22819

Even though Crowcoder's post provides a working code, it does not really explain too much IMHO.

Policy definition and execution

Polly was designed in a way that you define a policy first then you execute it. Most of the times these two steps are separated in time and space but there are occasions when you just want to do it right away.

var result = Policy
        .Handle<Exception>()           
        .RetryAsync(...)
        .ExecuteAsync(...);

I understand that many people got confused with these method chaining (two XYZAsync methods). This is mainly because of the absence of the Build method call. In the V7 (and prior) API you don't have an explicit I'm done with the policy definition, now I'm ready to execute it.

In the V8 API you have to explicitly call the Build on the ResiliencePipelineBuilder to get a ResiliencePipeline which can be executed.

onRetry{Async}

Polly was designed in a way to allow the library consumers to provide callbacks to react on certain actions. In the old .NET world these would be events but with the newer directions these are callbacks.

In other words Polly policies implement the template method design pattern. Each policy defines a set of statements that needs to be executed in a given order and provides customization points (in the design pattern they are called steps). In the original pattern the steps could be defined/overridden via inheritance. In case of Polly this is done via callback.

The onRetry{Async} callback is called

  • if a given attempt is failed and a new retry should be triggered (for example an exception thrown which is handled by the policy and the policy did not exceed the maximum retry count)
  • but before the sleep duration (if any)

If you define your retry policy with Retry{Async} then there is no delay between the retry attempts. If you use WaitAndRetry{Async} then you can instruct Polly to take a rest before kicking off a new attempt.

My main point here is that onRetry{Async} is a notification hook. So, normally you don't want to execute any business logic here rather just propagate the notification to a persistence storage for tracing.

onRetryAsync overloads

One of the biggest criticism against Polly V7: there were way too many overloads. Just look at this file: AsyncRetrySyntax. It contains almost 20 overloads

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, Func<int, TimeSpan> sleepDurationProvider)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, Func<int, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan> onRetry)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, Func<int, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, Task> onRetryAsync)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount,
    Func<int, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, Context> onRetry)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount,
    Func<int, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, Context, Task> onRetryAsync)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, Func<int, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, int, Context> onRetry)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount,
    Func<int, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, int, Context, Task> onRetryAsync)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount,
    Func<int, Context, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, Context> onRetry)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount,
    Func<int, Context, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, Context, Task> onRetryAsync)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, Func<int, Context, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, int, Context> onRetry)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount,
    Func<int, Context, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, int, Context, Task> onRetryAsync)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount,
    Func<int, Exception, Context, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, int, Context, Task> onRetryAsync)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, IEnumerable<TimeSpan> sleepDurations)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, IEnumerable<TimeSpan> sleepDurations, Action<Exception, TimeSpan> onRetry)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, IEnumerable<TimeSpan> sleepDurations, Func<Exception, TimeSpan, Task> onRetryAsync)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, IEnumerable<TimeSpan> sleepDurations, Action<Exception, TimeSpan, Context> onRetry)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, IEnumerable<TimeSpan> sleepDurations, Func<Exception, TimeSpan, Context, Task> onRetryAsync)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, IEnumerable<TimeSpan> sleepDurations, Action<Exception, TimeSpan, int, Context> onRetry)

public static AsyncRetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, IEnumerable<TimeSpan> sleepDurations, Func<Exception, TimeSpan, int, Context, Task> onRetryAsync)

It is pretty easy to got lost. Even for those who were using this API quite frequently.

Another big problem with this: it is also not extensibility friendly. Let me show you an example:

var retryPolicy = Policy
    .Handle<Exception>()
    .RetryAsync(3, onRetry: (exception, retryCount, context) =>
    {
        Console.WriteLine(exception.Message);
    });

Let's extend this with a new clause to trigger for negative numbers as well

var retryPolicy = Policy
    .Handle<Exception>()
    .OrResult<int>(Int32.IsNegative)
    .RetryAsync(3, onRetry: (exception, retryCount, context) =>
    {
        Console.WriteLine(exception.Message);
    });

This code won't compile anymore with the following error (pointing to the exception.Message):

DelegateResult<int> does not contain a definition for Message and no accessible extension method Message accepting a first argument of type DelegateResult<int> could be found (are you missing a using directive or an assembly reference?

So, what happened? The first version of the policy triggers only if an exception occurred. So, the first parameter of the onRetry was indeed an Exception.

The second version of the policy triggers either if an exception occurred or a negative number is returned. So, the first parameter of the onRetry now is DelegateResult<int> and not an Exception anymore.

The DelegateResult does not have a Message property rather a Result and an Exception. That's why we have the compilation error.


All in all it takes some time to get familiar with the V7 API and its design decisions. V8 is a complete rewrite which tried to address all of these problems.

Upvotes: 1

Crowcoder
Crowcoder

Reputation: 11514

I think it is unusual to run your logic in the retry policy - unless I misunderstand your question. More typically you execute the policy by calling a method that runs your logic.

Something like this:

async Task Main()
{
    var polly = Policy
        .Handle<Exception>()           
        .RetryAsync(3, (exception, retryCount, context) => Console.WriteLine($"try: {retryCount}, Exception: {exception.Message}"));

    var result = await polly.ExecuteAsync(async () => await DoSomething());
    Console.WriteLine(result);
}

int count = 0;

public async Task<string> DoSomething()
{
    if (count < 3)
    {
        count++;
        throw new Exception("boom");
    }
        
    return await Task.FromResult("foo");
}

output

try: 1, Exception: boom
try: 2, Exception: boom
try: 3, Exception: boom
foo

Upvotes: 40

Related Questions