I was used to WaitAndRetryForeverAsync
in the past which was wrong because I believe the Retry pattern is supposed to handle only transient faults, such as rate limiting, 429 status code, etc. At the moment that the API I was subscribing to went offline for service maintenance which took about 25 minutes, WaitAndRetryForeverAsync
was retrying forever in a constant interval (not exponential which doesn't really matter in this case) which in fact triggered some firewall rules on the API side and my IP was blocked for a while.
I'm trying to do what Nick Chapsas says in his Circuit Breaker video, i.e. if it fails to retry 5 times -> we make the assumption that the service is in maintenance. So enable the retries after 30 minutes and so on and so forth until it reconnects, even if it takes hours to do (depending on how long the service maintenance is).
The question is how do I achieve that circuit breaker policy after WaitAndRetry
's failure?
/// <summary>
/// This class provides Transient Fault Handling extension methods.
/// </summary>
internal static class Retry
{
public static void Do(Action action, TimeSpan retryInterval, int retryCount = 3)
{
_ = Do<object?>(() =>
{
action();
return null;
}, retryInterval, retryCount);
}
public static async Task DoAsync(Func<Task> action, TimeSpan retryInterval, int retryCount = 3)
{
_ = await DoAsync<object?>(async () =>
{
await action();
return null;
}, retryInterval, retryCount);
}
public static T Do<T>(Func<T> action, TimeSpan retryWait, int retryCount = 3)
{
var policyResult = Policy
.Handle<Exception>()
.WaitAndRetry(retryCount, retryAttempt => retryWait)
.ExecuteAndCapture(action);
if (policyResult.Outcome == OutcomeType.Failure)
{
throw policyResult.FinalException;
}
return policyResult.Result;
}
public static async Task<T> DoAsync<T>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 3)
{
var policyResult = await Policy
.Handle<Exception>()
.WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
.ExecuteAndCaptureAsync(action);
if (policyResult.Outcome == OutcomeType.Failure)
{
throw policyResult.FinalException;
}
return policyResult.Result;
}
}
Simplest solution
If you want to have the following delay sequences:
Then you can achieve this by introducing by the following helper method:
The usage is pretty simple:
Under this SO question I have showed a somewhat similar solution.
Sophisticated solution
By default the policies are unaware of each other, even if they are chained together via
PolicyWrap
. One way to exchange information between policies is the usage of Polly's Context.First let's define the Circuit Breaker
Open
then the Context will contain the sleep durationClosed
then the Context will not contain anymore the sleep durationPlease be aware that with this setup the delay sequence will look like this:
And finally let's define the retry policy
Under this SO question I have showed a somewhat similar solution.
UPDATE #1
If you want to have the same delay sequences like what we had under the simplest solution section but you want to use Circuit Breaker for that then you have to use some workaround.
The necessity of this workaround is that we have a
HalfOpen
state as well, not justClosed
andOpen
. The above delay sequences would be used out-of-the-box if we would have onlyClosed
andOpen
states. But after the break duration the Circuit Breaker transitions intoHalfOpen
(to allow a probe) rather than toClosed
.At first glance the Advanced Circuit Breaker could provide this "auto-reset feature" because of its
samplingDuration
extra parameter. But unfortunately the ACB also hasHalfOpen
state.The workaround is that we force the Circuit Breaker to transition back to
Closed
by explicitly calling theReset
function on it.So, the solution is the following:
onReset
to avoid race-conditionHalfOpen
Reset
on the circuit breakerWith this "trick" we could skip the
HalfOpen
state. Please note that even though theretry
has explicit reference to thecircuitBreaker
you still have to use thePolicyWrap
to make it work.