App-vNext / Polly

Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. From version 6.0.1, Polly targets .NET Standard 1.1 and 2.0+.

Home Page:https://www.thepollyproject.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Question]: DelegateResult.Result with Policy.HandleResult.

blankor1 opened this issue · comments

What are you wanting to achieve?

I am trying to achieve the retry with certain HTTP statuscode using Policy.HandleResult and I found the comments for the DelegateResult.Result says "The result of executing the delegate. Will be default(TResult) if an exception was thrown.":

/// The result of executing the delegate. Will be default(TResult) if an exception was thrown.

But if the exception is thrown, it should just terminate the retry and bubble up if we haven't set the Handle. Through my testing, the exception also terminate the retry. So I have some questions regarding this:

  1. What does this default(TResult) value in the XML comment means and what is its actual behavior.
  2. Why there is delegation here not just the Result type? Is this delegation from the ExecuteAsync()? If so, will this DelegateResult.Result block the async call?

Any insights would be appreciated. Thanks!

What code or approach do you have so far?

await Policy
        .HandleResult<Response>(response => this.ShouldRetryRequest(response)) // contains status code for retry logic
        .WaitAndRetryAsync(
            MaxRetryAttempt,
            sleepDurationProvider: (_, responseMessage, __) => this.GetSleepDurationBeforeRetry(responseMessage.Result),
            (responseMessageDelegation, _, retryCount, _) =>
            {
                ResponseMessage responseMessage = responseMessageDelegation.Result;
                // logic with the responseMessage

                return Task.CompletedTask;
            })
        .ExecuteAsync(async () => await SendRequest(request, cancellationToken));

Additional context

No response

But if the exception is thrown, it should just terminate the retry and bubble up if we haven't set the Handle.
Through my testing, the exception also terminate the retry.

That's exactly how it should work. If an exception is thrown by the decorated method and the policy is not defined in a way to handle that then it will propagate the exception to the Execute{Async} caller.

On the related wiki page there is a dedicated flow diagram about this

retry flow diagram

What does this default(TResult) value in the XML comment means and what is its actual behavior.

If you call the ExecuteAndCapture{Async} then you will receive an DelegateResult<TResult> object.

If an exception was thrown then the Exception property is populated with the thrown exception.

outcome = new DelegateResult<TResult>(handledException);

If a result is returned then the Result object is populated.

outcome = new DelegateResult<TResult>(result);

Why there is delegation here not just the Result type? Is this delegation from the ExecuteAsync()?

Sorry, but I don't understand what do you mean here. Could you please rephrase it?

If so, will this DelegateResult.Result block the async call?

No, it does not. That .Result is NOT called on a Task<TResult> rather than on the DelegateResult. The DelegateResult is populated after the decorated method is already awaited. I can understand the confusion with the unfortunate name choice.

@peter-csala Thank you for your detailed explanation!

So just to double confirm, only when we call the ExecuteAndCaptureAsync will we get the default TResult together with the exception in the DelegateResult. And if we just call ExecuteAsync, the exception will be thrown direct without creating a delegateResult. Is this correct?

Why there is delegation here not just the Result type? Is this delegation from the ExecuteAsync()?

Sorry, but I don't understand what do you mean here. Could you please rephrase it?

I see the point. Sry for the confusing. Please just ignore this.

So just to double confirm, only when we call the ExecuteAndCaptureAsync will we get the default TResult together with the exception in the DelegateResult. And if we just call ExecuteAsync, the exception will be thrown direct without creating a DelegateResult. Is this correct?

I have to confess that I mixed DelegateResult and PolicyResult in my previous answer. 😥
Let me correct myself and answer your question as well.

DelegateResult

The DelegateResult captures the outcome of the decorated method's execution. And that information is passed to the sleepDurationProvider callback method as well as to the onRetry{Async} callback method. In other words, you can access the outcome of the current failed attempt inside the sleepDurationProvider and calculate the new sleep duration accordingly. You can also access this information inside the onRetry{Async} method.

Please bear in mind that not every overload receives a DelegateResult parameter!

So, this type is used to pass information about the execution of the decorated method to those custom callbacks that you can define during the policy declaration. Its usage is not limited to the Retry. Fallback (onFallback{Async}, fallbackAction) and Circuit Breaker (onBreak) also utilize it.

PolicyResult

The PolicyResult{<TResult>} captures the outcome of the policy's execution. This type has two variants: PolicyResult and PolicyResult<TResult>.

  • The former one is used when the policy is defined without TResult generic parameter. So, this class either captures information about the thrown Exception OR simply just the fact that the policy execution was successful.
  • The latter one is used when the policy is defined with TResult generic parameter. So, this class either captures information about the thrown Exception OR captures information about the result of the policy execution.

These classes are used only by the ExecuteAndCapture{Async} overloads. The Execute{Async} overloads do NOT wrap up the outcome of the policy execution. They either return with Task<TResult>|TResult or throw the exception.

Original question

/// The result of executing the delegate. Will be default(TResult) if an exception was thrown.

The second statement is not Polly specific. It is how C# and its type system works.

Let's me demonstrate it with the following fiddle: https://dotnetfiddle.net/GbjCFC

var dr1 = new DelegateResult<int>(1);
var dr2 = new DelegateResult<string>("a");

var dr3 = new DelegateResult<int>(new Exception());
var dr4 = new DelegateResult<string>(new Exception());

dr1.Dump();
dr2.Dump();
dr3.Dump();
dr4.Dump();

In case of dr3 the Result will be 0 which is default(int).
In case of dr4 the Result will be null which is default(string).

Sorry for the confusion. I hope this explains everything. If you have further questions then please let me know.

I see. But for the sample code I provide in the question, if the SendRequest() method throw an exception, then it will not even create a DelegateResult and just terminate the Policy and throw the exception. Only when we are using Handle() and with some specific overload method will the DelegateResult be created and used. Is this correct?

I see. But for the sample code I provide in the question, if the SendRequest() method throw an exception, then it will not even create a DelegateResult and just terminate the Policy and throw the exception. Only when we are using Handle() and with some specific overload method will the DelegateResult be created and used. Is this correct?

Yes, that's correct. If the policy is not defined in a way to handle the thrown exception (either via the Handle or the Or builder methods) then it won't create a DelegateResult. As we discussed above, the DelegateResult is used by the sleepDurationProvider and by the onRetry{Async}. Since the exception is not handled from the policy perspective none of these custom defined delegates will be called.

Let me copy here the relevant code fragment:

catch (Exception ex)
{
    Exception handledException = shouldRetryExceptionPredicates.FirstMatchOrDefault(ex);
    if (handledException == null)
    {
        throw;
    }

    canRetry = tryCount < permittedRetryCount && (sleepDurationsEnumerator == null || sleepDurationsEnumerator.MoveNext());

    if (!canRetry)
    {
        handledException.RethrowWithOriginalStackTraceIfDiffersFrom(ex);
        throw;
    }

    outcome = new DelegateResult<TResult>(handledException);
}

The first throw; is called if the thrown exception is not a handled exception from the policy perspective

retry_flow_first_case

The second throw; is called if the thrown exception is handled but it run out of the allowed retry attempts.

retry_flow_second_case

I see, Thank you so much for the detailed explanation!