2

Given this code that tries to call AuthenticationManager.Authenticate() for 10 times, and then throws a CustomException after 10 failed attempts:

string res = Policy
    .Handle<Exception>() // Handles any exception
    .OrResult<string>(result => string.IsNullOrWhiteSpace(result)) // or if result is null
    .Fallback(() => throw new CustomException("Exception!")) // Not being thrown after 10 unsuccessful attempts
    .Wrap(
        Policy
            .Handle<Exception>()
            .OrResult<string>(result => string.IsNullOrWhiteSpace(result))
            .WaitAndRetry(
                10,
                retryAttempt => TimeSpan.FromSeconds(60),
                onRetry: (response, delay, retryCount, context) => Trace.WriteLine($"[{DateTime.UtcNow}] Authentication failed. Retrying after 60 seconds...(Attempt {retryCount} of 10)")))
    .ExecuteAndCapture(() => AuthenticationManager.Authenticate())
    .Result;

Why is the CustomException not being thrown from the fallback action? What should be the correct way to do it?

terrible-coder
  • 323
  • 2
  • 9
  • 18
  • Do you Unit-Test your Policies? _"Note that .ExecuteAndCapture/Async(...) captures whether the overall execution result is one that would have been considered a failure by the policy. Therefore, if your FallbackPolicy replaces a failure result with one that would be considered a success (as many fallback policies do - the "graceful degrade" pattern), then .ExecuteAndCapture/Async(...) will naturally report PolicyResult.Outcome == OutcomeType.Success."_ - [FallbackPolicy with ExecuteAndCapture()](https://github.com/App-vNext/Polly/wiki/Fallback#fallbackpolicy-with-executeandcapture) – Fildor May 21 '21 at 11:55
  • ^^could this be an issue here? – Fildor May 21 '21 at 11:56
  • Also found this issue: https://github.com/App-vNext/Polly/issues/477 – Fildor May 21 '21 at 12:00
  • I just put your code into a xUnit-Test and found, that it "kinda" works: `Outcome` is `Failure`, `Result` is `null` and `FinalException` is `CustomException`. – Fildor May 21 '21 at 12:26
  • In a second test, `Assert.Throws(() => policy.Execute(() => throw new Exception("Muh")));` passes. So, here the Exception is actually bubbled up. – Fildor May 21 '21 at 12:29
  • 1
    `ExecuteAndCapture` will not throw exception. It **captures** the result of the delegate even if is was `Tx` or any kind of `Exception`. The `Outcome` will tell you whether it succeeded or failed. In case of failure you can access the exception via the `FinalException`, [Reference](https://github.com/App-vNext/Polly#executeandcapturetresult) – Peter Csala May 21 '21 at 14:15

2 Answers2

2

These UnitTests pass: (xUnit)

[Fact]
public void Test1()
{
    var policy = Policy<string>
        .Handle<Exception>()
        .OrResult(result => string.IsNullOrWhiteSpace(result))
        .Fallback(() => throw new CustomException())
        .Wrap(
              Policy<string>
                 .Handle<Exception>()
                 .OrResult(result => string.IsNullOrWhiteSpace(result))
                 .WaitAndRetry(
                  10,
                  _ => TimeSpan.FromSeconds(1),
                  onRetry: (response, delay, retryCount, context) => Trace.WriteLine($"[{DateTime.UtcNow}] Authentication failed. Retrying after 1 second...(Attempt {retryCount} of 10)")
                  )
        );

    var result = policy.ExecuteAndCapture(() => throw new Exception("Muh"));
    Assert.IsType<CustomException>(result.FinalException);
    Assert.Null(result.Result);
    Assert.Equal(OutcomeType.Failure, result.Outcome);
}

[Fact]
public void Test2()
{
    var policy = Policy<string>
        .Handle<Exception>()
        .OrResult(result => string.IsNullOrWhiteSpace(result))
        .Fallback(() => throw new CustomException())
        .Wrap(
              Policy<string>
                 .Handle<Exception>()
                 .OrResult(result => string.IsNullOrWhiteSpace(result))
                 .WaitAndRetry(
                  10,
                  _ => TimeSpan.FromSeconds(1),
                  onRetry: (response, delay, retryCount, context) => Trace.WriteLine($"[{DateTime.UtcNow}] Authentication failed. Retrying after 1 second...(Attempt {retryCount} of 10)")
                  )
        );

    Assert.Throws<CustomException>(() => policy.Execute(() => throw new Exception("Muh")));
}

So, I figure, you could just use Execute, or check the Outcome and rethrow the Exception like so:

var result = policy.ExecuteAndCapture(() => AuthenticationManager.Authenticate());
if( result.Outcome == OutcomeType.Failure && result.FinalException is not null)
{
    throw result.FinalException;
}
return result.Result;
Fildor
  • 14,510
  • 4
  • 35
  • 67
0

Found that just using WaitAndRetry and just checking for the Outcome worked for me (in case anyone comes across this question):

var policyResult = Policy
    .Handle<Exception>()
    .OrResult<AuthenticationResult>(result => result is null)
        .WaitAndRetry(
            10,
            retryAttempt => TimeSpan.FromSeconds(60),
            onRetry: (response, delay, retryCount, context) => Trace.WriteLine($"[{DateTime.UtcNow}] Call failed. Retrying after 60 seconds...(Attempt {retryCount} of 10)"))
    .ExecuteAndCapture(() => AuthenticationManager.Authenticate());

if (policyResult.Outcome == OutcomeType.Failure)
{
    throw new CustomException("FAILED", policyResult.FinalException);
}

else
{
    string value = policyResult.FinalHandledResult;
}
terrible-coder
  • 323
  • 2
  • 9
  • 18