WHAT DO I HAVE NOW?
Currently, I have a client configured with a RetryAsync
policy that uses a primary address and on failure switches to a failover address. The connection details are read from a secrets manager.
services
.AddHttpClient ("MyClient", client => client.BaseAddress = PlaceholderUri)
.ConfigureHttpMessageHandlerBuilder (builder => {
// loads settings from secret manager
var settings = configLoader.LoadSettings().Result;
builder.PrimaryHandler = new HttpClientHandler {
Credentials = new NetworkCredential (settings.Username, settings.Password),
AutomaticDecompression = DecompressionMethods.GZip
};
var primaryBaseAddress = new Uri (settings.Host);
var failoverBaseAddress = new Uri (settings.DrHost);
builder.AdditionalHandlers.Add (new PolicyHttpMessageHandler (requestMessage => {
var relativeAddress = PlaceholderUri.MakeRelativeUri (requestMessage.RequestUri);
requestMessage.RequestUri = new Uri (primaryBaseAddress, relativeAddress);
return HttpPolicyExtensions.HandleTransientHttpError ()
.RetryAsync ((result, retryCount) =>
requestMessage.RequestUri = new Uri (failoverBaseAddress, relativeAddress));
}));
});
WHAT AM I TRYING TO ACHIEVE?
In general
My client can use a primary or failover service. When the primary is down, use failover till the primary is back up. When both are down, we get alerted and can change the service addresses dynamically via secrets manager.
In code
Now I would like to introduce also a CircuitBreakerPolicy
and chain those 2 policies together. I am looking for a configuration that is encapsulated and faults are handled on the client level and not on the class consuming that client.
Scenario explained
Let's assume that there is a circuit breaker policy wrapped in a retry policy with a single client.
The circuit breaker is configured to break the circuit for 60 seconds after 3 failed attempts on transient errors on the primary base address. OnBreak
- the address changes from primary to failover.
The retry policy is configured to handle BrokenCircuitException
, and retry once with the address changed from primary to failover to continue.
- Request on primary address - 500 code
- Request on primary address - 500 code
- Request on primary address - 500 code (3 consecutive failures reached)
- Circuit broken for 60 seconds
- Request on primary address -
BrokenCircuitException
caught by retry policy, call failover - Request on primary address -
BrokenCircuitException
caught by retry policy, call failover - Request on primary address -
BrokenCircuitException
caught by retry policy, call failover - Request on primary address -
BrokenCircuitException
caught by retry policy, call failover - (after 60 secs) Circuit half-open - (here can be broken for another 60 secs or is open - assume open)
- Request on primary address - 200 code
As described in this articles, there is a solution to this using a breaker wrapped in a fallback, but as you can see there, the logic for default and fallback are implemented in class and not on client level.
I would like
public class OpenExchangeRatesClient
{
private readonly HttpClient _client;
private readonly Policy _policy;
public OpenExchangeRatesClient(string apiUrl)
{
_client = new HttpClient
{
BaseAddress = new Uri(apiUrl),
};
var circuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 2,
durationOfBreak: TimeSpan.FromMinutes(1)
);
_policy = Policy
.Handle<Exception>()
.FallbackAsync(() => GetFallbackRates())
.Wrap(circuitBreaker);
}
public Task<ExchangeRates> GetLatestRates()
{
return _policy
.ExecuteAsync(() => CallRatesApi());
}
public Task<ExchangeRates> CallRatesApi()
{
//call the API, parse the results
}
public Task<ExchangeRates> GetFallbackRates()
{
// load the rates from the embedded file and parse them
}
}
to be rewritten as
public class OpenExchangeRatesClient
{
private readonly HttpClient _client;
public OpenExchangeRatesClient (IHttpClientFactory clientFactory) {
_client = clientFactory.CreateClient ("MyClient");
}
public Task<ExchangeRates> GetLatestRates () {
return _client.GetAsync ("/rates-gbp-usd");
}
}
WHAT HAVE I READ?
- How CircutBreakerWorks and what is there for
- Policies can be wrapped and there is a recommender order for wrapping
- Microsoft's example of Circuit breaker
- Other example of Circuit breaker
- Fallback Policy
WHAT HAVE I TRIED?
I have tried few different scenarios to chain and combine circuit breaker policy with a retry policy to achieve the desired goal on a client lever in the Startup file. The last state was the below. The policies are wrapped in the order where retry would be able to catch a BrokenCircuitException
, but this has not been the case. The Exception is thrown on the consumer class, which is not the desired result. Although RetryPolicy
is triggered, the exception on the consumer class is still thrown.
var retryPolicy = GetRetryPolicy();
var circuitBreaker = GetCircuitBreakerPolicy();
var policyWraper = Policy.WrapAsync(retryPolicy, circuitBreaker);
services
.AddHttpClient("TestClient", client => client.BaseAddress = GetPrimaryUri())
.AddPolicyHandler(policyWraper);
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
3,
TimeSpan.FromSeconds(45),
OnBreak,
OnReset,
OnHalfOpen);
}
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return Policy<HttpResponseMessage>
.Handle<Exception>()
.RetryAsync(1, (response, retryCount) =>
{
Debug.WriteLine("Retries on broken circuit");
});
}
I have left out the methods OnBreak
, OnReset
and OnHalfOpen
since they are just printing some messages.
UPDATE: Added Logs from Console.
Circuit broken (after 3 attempts)
Retries on broken
Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll
Retries on broken circuit
Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll
'CircuitBreakerPolicy.exe' (CoreCLR: clrhost): Loaded 'C:\Program Retries on broken circuit Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll
UPDATE 2: Added reference URL to the class making use of the client with policies configured
UPDATE 3: The project has been updated so that implementation of WeatherService2.Get
works in the desired way: When primary service is unavailable the circuit is broken, falover service is used till circuit is closed. That would be the answer to this question, however I would like to explore a solution, where same outcome is achieved using WeatherService.Get
with the appropriate policy and client setup on the Startup
.
Reference to class using the client. Reference to project using the class.
On the above logs can be seen Exception thrown: 'System.AggregateException' in System.Private.CoreLib.dll
which thrown by the circuitbreaker - that is not expected since there is retry wrapping the circuit breaker.