[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.":
Polly/src/Polly/DelegateResult.cs
Line 22 in 61a767e
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:
- What does this default(TResult) value in the XML comment means and what is its actual behavior.
- 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
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.
Polly/src/Polly/Retry/RetryEngine.cs
Line 63 in 61a767e
If a result is returned then the Result
object is populated.
Polly/src/Polly/Retry/RetryEngine.cs
Line 45 in 61a767e
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 defaultTResult
together with the exception in theDelegateResult
. And if we just callExecuteAsync
, the exception will be thrown direct without creating aDelegateResult
. 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 thrownException
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 thrownException
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
The second throw;
is called if the thrown exception is handled but it run out of the allowed retry attempts.
I see, Thank you so much for the detailed explanation!