Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Execute multiple policies

Tags:

c#

.net

polly

How to execute multiple policies (or combine them into a single one)?

For example I have:

var policy1 = Policy.Handle<DivideByZeroException>().WaitAndRetry(5));

var policy2 = Policy.Handle<StackOverflowException>().RetryForever();

How to apply them to one method at the same time?

like image 949
denny Avatar asked Feb 07 '17 15:02

denny


2 Answers

As of Polly v5.0, there's a new PolicyWrap class, which let you combine multiple policies.

var policy1 = Policy.Handle<DivideByZeroException>().WaitAndRetry(3, i => TimeSpan.FromSeconds(1));
var policy2 = Policy.Handle<StackOverflowException>().RetryForever();
PolicyWrap policyWrap = Policy.Wrap(policy1, policy2);
policyWrap.Execute(someGreatAction);
like image 89
Ofir Winegarten Avatar answered Nov 14 '22 20:11

Ofir Winegarten


There are several ways to combine two policies. Let me list all the different options for the sake of completeness. I will add recommendations for each: when to use it and when to avoid it

Branch on exception

ExceptionDispatchInfo edi = null;
int retryCounter = 0;

var combinedPolicy = Policy
       .Handle<DivideByZeroException>()
       .Or<StackOverflowException>()
       .WaitAndRetry(CalculateSleep());

combinedPolicy.Execute(() => {
    try
    {
        //User code...
        throw new DivideByZeroException();
    }
    catch (Exception ex)
    {
        edi = ExceptionDispatchInfo.Capture(ex);
        edi.Throw();
    }
});

IEnumerable<TimeSpan> CalculateSleep()
{
    while (true)
    {
        var wasItADivideByZero = edi.SourceException is DivideByZeroException;
        var attempt = retryCounter++;
        if (wasItADivideByZero)
        {
            if (attempt > 3) break;
            yield return TimeSpan.FromSeconds(1);
        }
        yield return TimeSpan.FromSeconds(0);
    }
}

Notes

  • The WaitAndRetry does not have an overload where you don't have to specify the retryCount but you can access the thrown exception inside the sleepDurations
  • So, the built-in options support: either use retryCount + sleepDurationProvider (from where you can access the exception) or use sleepDurations
  • That's why we have to use ExceptionDispatchInfo to capture the thrown exception and access it inside our custom CalculateSleep method

Use it

  • If you would have two policies with the same amount of maximum retry attempts but with different sleep and different trigger, like this
var combinedPolicy = Policy
       .Handle<DivideByZeroException>()
       .Or<StackOverflowException>()
       .WaitAndRetry(3,
       (_, ex, __) => TimeSpan.FromSeconds(ex is DivideByZeroException ? 1 : 2),
       (_, __, ___, ____) => { });

Avoid it

  • Do not re-invent the wheel if you have other alternatives
  • Even though the above solution works like a charm it is complex and hard to maintain

Explicit continuation

policy1.Execute(() =>
{
    policy2.Execute(() =>
    {
        //User code
    });
});

OR

policy2.Execute(() =>
{
    policy1.Execute(() =>
    {
        //User code
    });
});

Notes

  • The order does not matter here, because the two policies are independent
    • But please bear in mind that we have implemented here an escalation chain
  • So, if we would have a retry and a timeout policy then the order would matter
    • Outer: Timeout; Inner: Retry >> Timeout is a global, overarching
    • Outer: Retry; Inner: Timeout >> Timeout is local (so per attempt)

Use it

  • If you have no more than two policies and the to-be-decorated code is extracted into a separate method, like this
policy1.Execute(policy2.Execute(UserCode()));

Avoid it

  • Welcome to the callback hell / Pyramid of doom
  • If you want to / need to separate policy definition from its execution

The Wrap instance method

var combinedPolicy = policy1.Wrap(policy2);
combinedPolicy.Execute(UserCode());

OR

var combinedPolicy = policy2.Wrap(policy1);
combinedPolicy.Execute(UserCode());

Notes

  • We could separate the policy definition from its execution
  • The ordering does not matter here as well
  • The combinedPolicy's type is PolicyWrap, which implements the ISyncPolicy interface as well

Use it

  • When you want to separate the policy definition from its usage
  • It is suitable for combining no more than two policies

Avoid it

  • If you want to combine more than 3 then it looks ugly IMHO
var combinedPolicy = globalTimeout.Wrap(retry).Wrap(circuitBreaker).Wrap(localTimeout);
  • If you have a policy for a void and another policy for TResult then the system tries to act smart
    • The type of the combinedPolicy is now PolicyWrap<string>
  • This combination is may or may not be intentional
    • If unintentional then it will not warn you about it
var policy1 = Policy.Handle<DivideByZeroException>().WaitAndRetry(3, i => TimeSpan.FromSeconds(1));
var policy2 = Policy<string>.Handle<StackOverflowException>().RetryForever();

var combinedPolicy = policy1.Wrap(policy2);
string? result = combinedPolicy.Execute(() => "dummy response");

The Wrap static method

var combinedPolicy = Policy.Wrap(policy2, policy1);

OR

var combinedPolicy = Policy.Wrap(policy2, policy1);

Notes

  • This static method can receive any number of ISyncPolicy or ISyncPolicy<TResult> policies

Use it

  • With this method you could not combine a ISyncPolicy and a ISyncPolicy<string> policies because all policies should have the same type
var combinedPolicy = Policy.Wrap<HttpResponseMessage>(globalTimeout,retry,circuitBreaker,localTimeout);

Avoid it

  • When you want to explicitly combine two or more policies where you have a TResult and one or more void
    • Or multiple TResult and one or more void
  • Please note: You can not combine two policies where you have TResult1 and TResult2
like image 1
Peter Csala Avatar answered Nov 14 '22 19:11

Peter Csala