Search code examples
f#fallbackpollyretry-logicpolicywrap

Combining Polly policies and accessing handled exception in Polly policy Fallback


Given the following F# snippets

//User Code
.. code that can throw exceptions
"Success"

P1 policy

Policy
    .Handle<CosmosException>(fun cx -> cx.StatusCode = HttpStatusCode.TooManyRequests)
    .WaitAndRetryForever((fun _ cx _ -> (cx :?> CosmosException).RetryAfter.Value), (fun _ _ _ _ -> ()))

P2 policy

Policy<string>
    .Handle<Exception>()
    .Fallback("Failure")

P3 Policy

Policy<string>
    .Handle<Exception>()
    .Fallback(fun ex -> ex.Message)

Question #1 - How to combine P1 and P2?

Combine P1 and P2 in a policy P so that:

  • if User Code succeeds, "Success" is returned to the caller
  • if User Code throws CosmosException, P retries forever using the returned RetryAfter TimeSpan
  • if User Code throws any other exception, "Failure" is returned to the caller

Question # 2 - How to write P3?

There doesn't seem to be a Fallback overload that allows to access the handled exception to use it when constructing the fallback return value

Final scope is to combine P1 and P3 to obtain a policy PFinal such that:

  • if User Code succeeds, "Success" is returned to the caller
  • if User Code throws CosmosException, PFinal retries forever using the returned RetryAfter TimeSpan
  • if User Code throws any other exception, the handled exception message is returned to the caller

Solution

  • Answer to question 1

    In order to be able to chain policies you need to define them as compatible policies. Your p2 returns a string whereas your p1 returns nothing. So, you need to change p1 to return with string as well. Then you can use Policy.Wrap to define chaining, escalation.

    I'm not an F# developer so I will present the solution in C#. But the idea is the same in both languages:

    var p1 = Policy<string>
        .Handle<CosmosException>(ex => ex.StatusCode == HttpStatusCode.TooManyRequests)
        .WaitAndRetryForever(sleepDurationProvider: (_, dr, __) => ((CosmosException)dr.Exception).RetryAfter.Value,
                            onRetry: (_, __, ___) => { });
    var p2 = Policy<string>
        .Handle<Exception>()
        .Fallback("Failure");
    
    var p = Policy.Wrap(p1, p2);
    
    • Please note that we have to change Policy to Policy<string> in p1
    • Also note that the sleepDurationProvider will not receive the Exception as a parameter
      • Rather it will receive a DelegateResult<string> which have two mutually exclusive properties: Exception and Result

    Answer to question 2

    • There is an overload, where the fallbackAction delegate receives a DelegateResult<string> as a parameter
    var p2 = Policy<string>
        .Handle<Exception>()
        .Fallback(fallbackAction: (dr, _, __) => dr.Exception.Message,
                  onFallback: (_, __) => { });
    

    Update #1: Providing clarity

    Changing the p1 definition from Policy to Policy<string> has another implication as well: Your to be decorated code should return a string (namely "Success")

    Before the change:

    //user code
    
    //to be decorated code which might throw exception
    
    return "Success";
    

    After the change:

    //user code
    
    //The decorated code either return "Success" or throws CosmosException 
    return combinedPolicy.Execute(...
    

    Update #2: Fix ordering

    I suggested to chain the p1 and p2 with Policy.Wrap. Unfortunately I have showed you the wrong order Policy.Wrap(p1, p2). The correct one is Policy.Wrap(p2, p1) since the right most parameter is the most inner and the left most is the most outer. So, in your case the retry is the inner and the fallback is the outer.

    Apologise for the inconvenience.