I coded a resilience strategy based on retry, and a circuit-breaker policies. Now working, but with a issue in its behavior.
I noticed when the circuit-breaker is on half-open
, and the onBreak()
event is being executed again to close the circuit, one additional retry is triggered for the retry policy (this one aside of the health verification
for the half-open
status).
Let me explain step by step:
I've defined two strongly-typed policies for retry, and circuit-breaker:
static Policy<HttpResponseMessage> customRetryPolicy;
static Policy<HttpResponseMessage> customCircuitBreakerPolicy;
static HttpStatusCode[] httpStatusesToProcess = new HttpStatusCode[]
{
HttpStatusCode.ServiceUnavailable, //503
HttpStatusCode.InternalServerError, //500
};
Retry policy is working this way: two (2) retry per request, waiting five (5) second between each retry. If the internal circuit-breaker is open, must not retry. Retry only for 500, and 503 Http statuses.
customRetryPolicy = Policy<HttpResponseMessage>
//Not execute a retry if the circuit is open
.Handle<BrokenCircuitException>( x =>
{
return !(x is BrokenCircuitException);
})
//Stop if some inner exception match with BrokenCircuitException
.OrInner<AggregateException>(x =>
{
return !(x.InnerException is BrokenCircuitException);
})
//Retry if status are:
.OrResult(x => { return httpStatusesToProcess.Contains(x.StatusCode); })
// Retry request two times, wait 5 seconds between each retry
.WaitAndRetry( 2, retryAttempt => TimeSpan.FromSeconds(5),
(exception, timeSpan, retryCount, context) =>
{
System.Console.WriteLine("Retrying... " + retryCount);
}
);
Circuit-breaker policy is working in this way: Allow max three (3) failures in a row, next open the circuit for thirty (30) seconds. Open circuit ONLY for HTTP-500.
customCircuitBreakerPolicy = Policy<HttpResponseMessage>
// handling result or exception to execute onBreak delegate
.Handle<AggregateException>(x =>
{ return x.InnerException is HttpRequestException; })
// just break when server error will be InternalServerError
.OrResult(x => { return (int) x.StatusCode == 500; })
// Broken when fail 3 times in a row,
// and hold circuit open for 30 seconds
.CircuitBreaker(3, TimeSpan.FromSeconds(30),
onBreak: (lastResponse, breakDelay) =>{
System.Console.WriteLine("\n Circuit broken!");
},
onReset: () => {
System.Console.WriteLine("\n Circuit Reset!");
},
onHalfOpen: () => {
System.Console.WriteLine("\n Circuit is Half-Open");
});
Finally, those two policies are being nested this way:
try
{
customRetryPolicy.Execute(() =>
customCircuitBreakerPolicy.Execute(() => {
//for testing purposes "api/values", is returning 500 all time
HttpResponseMessage msResponse
= GetHttpResponseAsync("api/values").Result;
// This just print messages on console, no pay attention
PrintHttpResponseAsync(msResponse);
return msResponse;
}));
}
catch (BrokenCircuitException e)
{
System.Console.WriteLine("CB Error: " + e.Message);
}
What is the result that I expected?
Look at the images:
I'm trying to understand this behavior. Why one additional retry is being executed when the circuit-breaker is open for second, third,..., N time?
I've reviewed the machine state model for the retry, and circuit-breaker policies, but I don't understand why this additional retry is being performed.
Flow for the circuit-breaker: https://github.com/App-vNext/Polly/wiki/Circuit-Breaker#putting-it-all-together-
Flow for the retry policy: https://github.com/App-vNext/Polly/wiki/Retry#how-polly-retry-works
This really matter, because the time for the retry is being awaited (5 seconds for this example), and at the end, this is a waste of time for high-concurrency.
Any help / direction, will be appreciated. Many thanks.
With Polly.Context you can exchange information between the two policies (in your case: Retry and Circuit Breaker). The Context is basically a Dictionary<string, object>
.
So, the trick is to set a key on the onBreak
then use that value inside the sleepDurationProdiver
.
Let's start with inner Circuit Breaker policy:
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return Policy<HttpResponseMessage>
.HandleResult(res => res.StatusCode == HttpStatusCode.InternalServerError)
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(2),
onBreak: (dr, ts, ctx) => { ctx[SleepDurationKey] = ts; },
onReset: (ctx) => { ctx[SleepDurationKey] = null; });
}
Open
state for 2 seconds before it transits to HalfOpen
durationOfBreak
valueClosed
state (onReset
) it deletes this valueNow, let's continue with the Retry policy:
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return Policy<HttpResponseMessage>
.HandleResult(res => res.StatusCode == HttpStatusCode.InternalServerError)
.Or<BrokenCircuitException>()
.WaitAndRetryAsync(4,
sleepDurationProvider: (c, ctx) =>
{
if (ctx.ContainsKey(SleepDurationKey))
return (TimeSpan)ctx[SleepDurationKey];
return TimeSpan.FromMilliseconds(200);
},
onRetry: (dr, ts, ctx) =>
{
Console.WriteLine($"Context: {(ctx.ContainsKey(SleepDurationKey) ? "Open" : "Closed")}");
Console.WriteLine($"Waits: {ts.TotalMilliseconds}");
});
}
BrokenCircuitException
Open
state) then it returns with 200 millisecondsOpen
state) then it returns with value from the context
onRetry
only for debugging purposesFinally let's wire up the policies and test it
const string SleepDurationKey = "Broken";
static HttpClient client = new HttpClient();
static async Task Main()
{
var strategy = Policy.WrapAsync(GetRetryPolicy(), GetCircuitBreakerPolicy());
await strategy.ExecuteAsync(async () => await Get());
}
static Task<HttpResponseMessage> Get()
{
return client.GetAsync("https://httpstat.us/500");
}
Get
method in an asynchronous wayhandledEventsAllowedBeforeBreaking
is 2The output
Context: Closed
Waits: 200
Context: Open
Waits: 2000
Context: Open
Waits: 2000
Context: Open
Waits: 2000
handledEventsAllowedBeforeBreaking
is 3The output
Context: Closed
Waits: 200
Context: Closed
Waits: 200
Context: Open
Waits: 2000
Context: Open
Waits: 2000
handledEventsAllowedBeforeBreaking
is 4The output
Context: Closed
Waits: 200
Context: Closed
Waits: 200
Context: Closed
Waits: 200
Context: Open
Waits: 2000