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
Openthen the Context will contain the sleep durationClosedthen 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
HalfOpenstate as well, not justClosedandOpen. The above delay sequences would be used out-of-the-box if we would have onlyClosedandOpenstates. 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
samplingDurationextra parameter. But unfortunately the ACB also hasHalfOpenstate.The workaround is that we force the Circuit Breaker to transition back to
Closedby explicitly calling theResetfunction on it.So, the solution is the following:
onResetto avoid race-conditionHalfOpenReseton the circuit breakerWith this "trick" we could skip the
HalfOpenstate. Please note that even though theretryhas explicit reference to thecircuitBreakeryou still have to use thePolicyWrapto make it work.