11

I'm creating a retry policy the following way:

var policy = Policy.Handle<Exception>().WaitAndRetryAsync...

How to chail/build a timeout for the retrypolicy above? Policy.TimeoutAsync returns a TimeoutPolicy, hence I'm not able to do something like

var policy = Policy.TimeoutAsync(30).Handle<Exception>().WaitAndRetryAsync...

Does the timeout become a common setting for all my retry policies?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Johan
  • 35,120
  • 54
  • 178
  • 293

1 Answers1

36

To combine policies, you build each policy separately, then combine them using PolicyWrap.

To build an overall timeout which applies across all retries as a whole (ie across the entire operation):

var overallTimeoutPolicy = Policy.TimeoutAsync(60); 
var waitAndRetryPolicy = Policy
    .Handle<WhateverException>()
    .WaitAndRetryAsync(/* your wait-and-retry specification*/);
var combinedPolicy = overallTimeoutPolicy.WrapAsync(waitAndRetryPolicy);

await combinedPolicy.ExecuteAsync(cancellationToken => ...)

To impose a timeout on each specific try, wrap the retry and timeout policies in the other order:

var timeoutPerTry = Policy.TimeoutAsync(10); 
var waitAndRetryPolicy = Policy
    .Handle<WhateverException>()
    .WaitAndRetryAsync(/* your wait-and-retry specification*/);
var combinedPolicy = waitAndRetryPolicy.WrapAsync(timeoutPerTry);

await combinedPolicy.ExecuteAsync(cancellationToken => ...);

Or even use both kinds of timeout (per-try, per-overall-operation):

var overallTimeout = Policy.TimeoutAsync(60); 
var timeoutPerTry = Policy.TimeoutAsync(10); 
var waitAndRetryPolicy = Policy
    .Handle<WhateverException>()
    .WaitAndRetryAsync(/* your wait-and-retry specification*/);

var combinedPolicy = Policy
    .WrapAsync(overallTimeout, waitAndRetryPolicy, timeoutPerTry); // demonstrates alternative PolicyWrap syntax

await combinedPolicy.ExecuteAsync(cancellationToken => ...);

The PolicyWrap wiki gives full syntax details, and advice on the effects of different orderings, when combining policies within a wrap.


To answer:

Does the timeout become a common setting for all my retry policies?

Policies apply wherever you use them (whether used individually, or as part of a PolicyWrap).

You can thread-safely use any TimeoutPolicy instance you have configured at multiple call sites. So, to apply that timeout as a common setting for all your retry policies, simply include that same TimeoutPolicy instance in the PolicyWrap for each call site. The single TimeoutPolicy instance can safely be wrapped with different retry policy instances, if desired.

If both your wait-and-retry specification, and timeout specification, are common for all call sites, simply make one PolicyWrap instance encompassing both (per above code examples), and re-use that PolicyWrap instance everywhere. Again - thread safe.

mountain traveller
  • 7,591
  • 33
  • 38
  • Thank you for your detailed answer - much appreciated! – Johan Apr 04 '17 at 13:08
  • Hi agian, based on your comment, shouldn't this be working? https://pastebin.com/E6RaTWCn A one second timeout policy on a 2 second task. I'm expecting to see "fail: timeoutexception" in the log. Also, I noticed that you advice against `ExecuteAndCapture` with policywraps. What will happen if the last attempt throws an exception if I switch to `Execute`? – Johan Apr 05 '17 at 12:49
  • Hi. See Polly's [Timeout doco](https://github.com/App-vNext/Polly/wiki/Timeout). `TimeoutStrategy.Optimistic` (the default) operates by [co-operative cancellation](https://msdn.microsoft.com/en-us/library/dd997364.aspx) via `CancellationToken`. `TimeoutStrategy.Pessimistic` doesn't: it 'walks away' from even an uncancellable execution. `Thread.Sleep(...)` in your pastebin is uncancellable. Change Sleep() to `Task.Delay(...)` & use `ExecuteAsync()` overload taking a `CancellationToken`, per my examples: `ExecuteAsync(ct => Task.Delay(TimeSpan.FromSeconds(2), ct), someCancellationToken)`. – mountain traveller Apr 05 '17 at 17:40
  • OR, stick with `Thread.Sleep(...)`, and prove to yourself that the `TimeoutPolicy` with`TimeoutStrategy.Pessimistic` successfully times out on this - that is, makes the caller 'walk away' from waiting for the uncancellable `Thread.Sleep(...)`. Cancellable tasks and `TimeoutStrategy.Optimistic` are preferred and more resource friendly - see wiki. – mountain traveller Apr 05 '17 at 17:43
  • Re "What will happen if the last attempt throws an exception if I switch to Execute?" -> The policy will let that exception propagate. Presumably you want to catch that exception, and your code should decide how to proceed. The exception being rethrown is there to tell you the operation did not succeed within the configured retry attempts. – mountain traveller Apr 05 '17 at 17:50
  • Example showing `TimeoutStrategy.Pessimistic`: https://dotnetfiddle.net/xREDBf . (I reduced delays and numbers of tries, only to fit within the execution boundaries of dotnetfiddle) – mountain traveller Apr 05 '17 at 18:11
  • Example showing `TimeoutStrategy.Optimistic`: https://dotnetfiddle.net/pnMdSQ . Many more examples in Polly's [Timeout unit tests](https://github.com/App-vNext/Polly/blob/3143412a47bcc34f6274d75b3bd814b3e1a68f25/src/Polly.SharedSpecs/Timeout/TimeoutAsyncSpecs.cs) too. – mountain traveller Apr 05 '17 at 18:19
  • A followup question if you have time: In my actual code I use a synchronous webservice call instead of the timeout. Since it's synchronous, i.e. not an awaitable task, I suppose a pessimistic timeout strategy is appropriate? – Johan Apr 06 '17 at 11:11
  • 1
    Check if it has a timeout anyway. If not, pessimistic timeout allows caller to walk away after the timeout time. But note that's exactly what it does. The caller can walk away - but it doesn't magically cancel the timed-out call. Nuances all discussed in [wiki](https://github.com/App-vNext/Polly/wiki/Timeout#what-happens-to-the-timed-out-delegate) and the excellent [Stephen Toub article](https://blogs.msdn.microsoft.com/pfxteam/2012/10/05/how-do-i-cancel-non-cancelable-async-operations/) I point to. HTH – mountain traveller Apr 06 '17 at 16:36
  • You need to add `.Or()` to the handler if you want to retry the timed out attempt. – carlin.scott Oct 20 '20 at 00:08
  • https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry/ var retryWithBackoff = Policy .Handle() .WaitAndRetryAsync(Backoff.ExponentialBackoff(TimeSpan.FromSeconds(1), retryCount: 5)); var timeout = Policy.Timeout(TimeSpan.FromSeconds(45)); var retryWithBackoffAndOverallTimeout = timeout.Wrap(retryWithBackoff); When the combined time taken to make tries and wait between them exceeds 45 seconds, the TimeoutPolicy will be invoked and cause the current try and further retries to be abandoned. – Ayushmati Jan 27 '21 at 10:45