'Polly -how do I log final error and continue?

I'm trying to set up Polly in .Net Core 3.1 (Azure Functions v3).

I want to create a Policy in the Startup class which I can inject into functions.

The behaviour that I'm looking for is: It should wait And retry 3 times - if the final try fails - it should catch and log the exception (Serilog) otherwise just continue.

At the moment I have the following piece of code:

AsyncRetryPolicy emailRetryPolicy = Policy.Handle<Exception>()
    .WaitAndRetryAsync(3, retryAttempt =>
     TimeSpan.FromMilliseconds(retryAttempt * 500));

As you see I miss the part where it logs the error after 3 tries - and just continue. Any Idea how to do that?

Serilog is also added in Startup class (Partial code shown)

builder.Services.AddLogging(al => al.AddSerilog(logger));

I'm going to send an email with MailKit and want to log if it for some reasons fails. It should not stop the program for running.



Solution 1:[1]

The retry policy works in the following way:

  • It tries to execute the action
    • If it fails and the policy handles it then it will wait a predefined amount of time before it makes the next try
    • It if fails and the policy does not handle it then it will return with the original value (which can be an exception)
  • After the max retries has been reached (1st initial attempt + n retry attempts) and it still fails then it will return with the original value

Depending on your business logic, which you would like to wrap with this retry policy, you have the following two options:

  1. If it is a function, then you can use a Fallback policy. (Obviously if you can provide a fallback value.) In your Fallback you do the logging and then return with the fallback value. You can chain these two policies with the Policy.WrapAsync, but be aware of their order.

Sample fallback policy:

var fallbackPolicy = Policy<YourResponse>
    .Handle<Exception>()
    .FallbackAsync( (ct) =>
    {
         logger.LogError("Operation has been failed after several retries ...");
         return Task.FromResult(new YourResponse { ... });
    });
                
var combinedPolicy = Policy.WrapAsync(fallbackPolicy, retryPolicy);
  1. If it a method then you can use the onRetryAsync callback of the AsyncRetryPolicy

Sample retry policy:

var retryPolicy = Policy
    .Handle<Exception>()
    .WaitAndRetryAsync(
        settings.RetryCount,
        _ => TimeSpan.FromMilliseconds(settings.SleepDurationInMilliseconds),
        onRetryAsync: (ex, count, context) =>
            logger.LogError("Operation has been failed after several retries ...");
    );

I would like to emphasize that retry policy is designed for idempotent operations. Any service call, which might send an e-mail as a side-effect is not an idempotent operation.

Solution 2:[2]

Also had that issue and it took me some time to figure this out, so sharing my solution here. It's important to note that the order of policies wrapped matters

My case:

var retryPolicy = Policy
    .Handle<EmptyResponseException>()
    .Or<Exception>()
    .Or<HttpOperationException>()
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: _ => TimeSpan.FromSeconds(3),
        onRetryAsync: (e, i) => LogHandler.WarningAsync("Retrying due to " + e.Message + " Try " + i + " next."));
var fallbackPolicy = Policy.Handle<EmptyResponseException>()
    .Or<Exception>()
    .Or<HttpOperationException>()
    .FallbackAsync
    (
        async ct =>
            {
                await Task.FromResult(true);
                await LogHandler.CriticalAsync("i'm a fallback action");
                /* do something else async if desired */
            },
        async ex =>
        {
            await LogHandler.CriticalAsync("i'm an actual fallback");
            //throw ex; <<< if you throw here, the fallback action won't happen
        }

    );

_policy = Policy.WrapAsync(fallbackPolicy, retryPolicy);

Result:

logs

It doesn't quite matter what the fallbackPolicy or retryPolicy are in my case, but if I were to use this instead:

_policy = Policy.WrapAsync(retryPolicy, fallbackPolicy);

We'd get the fallback execute first, and the retry wouldn't work:

enter image description here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Duck Ling