private void ThrowOnNotRegistered(string key, out ICircuitBreakerPolicy policy) { if (!Registry.TryGet(key, out policy)) { throw new ArgumentException($"No circuitbreaker registered with this key {key}", nameof(key)); } }
public async Task HttpClientFactory_ApplyMultiplePolicies() { IServiceCollection services = new ServiceCollection(); ///// ADD HTTP CLIENT VIA HTTPCLIENTFACTORY AND POLICIES // this would typically happen in Startup.ConfigureServices // using a named client, can specify a <TClient> to inject it into a specific class // setup circuit breaker seperately so you can maintain a pointer to it for testing (validate state, reset, etc) var circuitBreakerPolicy = DemoPolicyFactory.GetHttpCircuitBreakerPolicy(); ICircuitBreakerPolicy <HttpResponseMessage> circuitBreakerPointer = (ICircuitBreakerPolicy <HttpResponseMessage>)circuitBreakerPolicy; services.AddHttpClient("CustomerService", client => { client.BaseAddress = new Uri(DemoHelper.DemoBaseUrl); client.Timeout = TimeSpan.FromSeconds( 3); // this will apply as an outer timeout to the entire request flow, including waits, retries,etc }) // add policies from outer (1st executed) to inner (closest to dependency call) // all policies must implement IASyncPolicy .AddPolicyHandler(DemoPolicyFactory.GetHttpFallbackPolicy("Default!")) .AddPolicyHandler(DemoPolicyFactory.GetHttpRetryPolicy()) .AddPolicyHandler(circuitBreakerPolicy) .AddPolicyHandler(DemoPolicyFactory.GetHttpInnerTimeoutPolicy()); ///// TEST POLICIES ///// await VerifyResiliencyPolicies(services, circuitBreakerPointer); }
// extracted this out to a seperate method since the above 2 demos are accomplishing the exact same thing private static async Task VerifyResiliencyPolicies(IServiceCollection services, ICircuitBreakerPolicy <HttpResponseMessage> circuitBreakerPointer) { // you would typically inject an IHttpClientFactory (if using named clients) or HttpClient (if not) into your classes that need them var serviceProvider = services.BuildServiceProvider(); var clientFactory = serviceProvider.GetService <IHttpClientFactory>(); // create client using factory var httpClient = clientFactory.CreateClient("CustomerService"); // verify happy path var responseMessage = await httpClient.GetStringAsync("api/demo/success"); Assert.AreEqual("Success!", responseMessage); // verify retry responseMessage = await httpClient.GetStringAsync("api/demo/error?failures=1"); Assert.AreEqual("Success!", responseMessage); // verify timeout responseMessage = await httpClient.GetStringAsync("api/demo/slow"); Assert.AreEqual("Default!", responseMessage); // verify circuit breaker for (int i = 0; i < 5; i++) { await httpClient.GetStringAsync("api/demo/error"); } Assert.AreEqual(CircuitState.Open, circuitBreakerPointer.CircuitState); }
public void Should_be_able_to_use_LastHandledResult_via_interface() { ICircuitBreakerPolicy <ResultPrimitive> breaker = Policy .HandleResult(ResultPrimitive.Fault) .CircuitBreaker(2, TimeSpan.FromMinutes(1)); breaker.LastHandledResult.Should().Be(default(ResultPrimitive)); }
public void Should_be_able_to_use_LastException_via_interface() { ICircuitBreakerPolicy breaker = Policy .Handle <DivideByZeroException>() .CircuitBreaker(2, TimeSpan.FromMinutes(1)); breaker.LastException.Should().BeNull(); }
public void Should_be_able_to_use_CircuitState_via_interface() { ICircuitBreakerPolicy breaker = Policy .Handle <DivideByZeroException>() .CircuitBreaker(2, TimeSpan.FromMinutes(1)); breaker.CircuitState.Should().Be(CircuitState.Closed); }
public SendToEndpointPolicy( ICompassEnvironment compassEnvironment, ICircuitBreakerPolicy circuitBreakerPolicy ) { _compassEnvironment = compassEnvironment; _circuitBreakerPolicy = circuitBreakerPolicy; }
public async Task AddResiliencyToHttpCall() { ///////////////////////// OUTER TIMEOUT POLICY ///////////////////////// // for sync code, you should replace this by setting the timeout on the HttpClient IAsyncPolicy <HttpResponseMessage> outerTimeoutPolicy = Policy .TimeoutAsync <HttpResponseMessage>(TimeSpan.FromSeconds(10), TimeoutStrategy.Optimistic, onTimeoutAsync: PolicyLoggingHelper.LogTimeoutAsync); ///////////////////////// RETRY POLICY ///////////////////////// // we are retrying on inner timeout, this might be a bad idea for many scenerios // you never want to retry on a circuit breaker exception IAsyncPolicy <HttpResponseMessage> retryPolicy = HttpPolicyExtensions .HandleTransientHttpError() .Or <TimeoutRejectedException>() .WaitAndRetryAsync(1, retryAttempt => TimeSpan.FromMilliseconds(500), onRetryAsync: PolicyLoggingHelper.LogWaitAndRetryAsync); ///////////////////////// CIRCUIT BREAKER POLICY ///////////////////////// // typical duration of break should be 30-60 seconds. Using 5 for this demo to make demo run faster IAsyncPolicy <HttpResponseMessage> circuitBreakerAsyncPolicy = Policy .HandleResult <HttpResponseMessage>(resp => !resp.IsSuccessStatusCode) // consider if you want to ignore 404s .Or <Exception>() .CircuitBreakerAsync(5, TimeSpan.FromSeconds(5), onBreak: PolicyLoggingHelper.LogCircuitBroken, onReset: PolicyLoggingHelper.LogCircuitReset) .WithPolicyKey(nameof(circuitBreakerAsyncPolicy)); // pointer to the circuit breaker so we can read/update its state as needed (haven't figured out a better way to do this) // this is EXTREMELY useful for resetting between unit tests (expose a reset method to allow callers to reset your circuit breaker) ICircuitBreakerPolicy <HttpResponseMessage> circuitBreaker = (ICircuitBreakerPolicy <HttpResponseMessage>)circuitBreakerAsyncPolicy; ///////////////////////// INNER TIMOUT POLICY (Optional) ///////////////////////// // inner timeout is totally optional and depends on use case. // The outer timeout is the most important for requests with callers waiting // Sometimes you might want an inner timeout slightly lower than the outer just so the circuit breaker will break on too many timeouts IAsyncPolicy <HttpResponseMessage> innerTimeoutPolicy = Policy .TimeoutAsync <HttpResponseMessage>(TimeSpan.FromSeconds(3), TimeoutStrategy.Optimistic, onTimeoutAsync: PolicyLoggingHelper.LogTimeoutAsync); ///////////////////////// POLICY WRAP ///////////////////////// // wrap policies from outer to inner var commonResiliencyPolicy = Policy .WrapAsync <HttpResponseMessage>(outerTimeoutPolicy, retryPolicy, circuitBreakerAsyncPolicy, innerTimeoutPolicy); ///////////////////////// FALLBACK POLICY (Wraps wrapped policy) ///////////////////////// // alternative would be to wrap ExecuteAsync in try/catch and return default from there // I am wrapping the above resiliency policy to show that you can wrap a wrapped policy inside another policy // Fallback is a good use case for this since it can vary a lot between different calls IAsyncPolicy <HttpResponseMessage> resiliencyPolicyWithFallback = HttpPolicyExtensions .HandleTransientHttpError() .Or <TimeoutRejectedException>() .Or <BrokenCircuitException>() .FallbackAsync(new HttpResponseMessage { // fetch default values or execute whatever behavior you want to fall back to Content = new StringContent("Default!") }, onFallbackAsync: PolicyLoggingHelper.LogFallbackAsync) .WrapAsync(commonResiliencyPolicy); // verify retry on error var response = await resiliencyPolicyWithFallback.ExecuteAsync((ctx, ct) => DemoHelper.DemoClient.GetAsync("api/demo/error?failures=1", ct), new Context($"{nameof(AddResiliencyToHttpCall)}_1"), CancellationToken.None); Assert.IsTrue(response.IsSuccessStatusCode); // verify retry on client timeout response = await resiliencyPolicyWithFallback.ExecuteAsync((ctx, ct) => DemoHelper.DemoClient.GetAsync("api/demo/slow?failures=1", ct), new Context($"{nameof(AddResiliencyToHttpCall)}_2"), CancellationToken.None); Assert.IsTrue(response.IsSuccessStatusCode); // verify default if every call times out DemoHelper.Reset(); response = await resiliencyPolicyWithFallback.ExecuteAsync((ctx, ct) => DemoHelper.DemoClient.GetAsync("api/demo/slow", ct), new Context($"{nameof(AddResiliencyToHttpCall)}_3"), CancellationToken.None); var result = await response.Content.ReadAsStringAsync(); Assert.AreEqual("Default!", result); // verify circuit broken on errors for (int i = 0; i < 6; i++) { response = await resiliencyPolicyWithFallback.ExecuteAsync((ctx, ct) => DemoHelper.DemoClient.GetAsync("api/demo/error", ct), new Context($"{nameof(AddResiliencyToHttpCall)}_4"), CancellationToken.None); result = await response.Content.ReadAsStringAsync(); Assert.AreEqual("Default!", result); } Assert.AreEqual(CircuitState.Open, circuitBreaker.CircuitState); // let circuit breaker reset (goes to 1/2 open after 5 seconds) await Task.Delay(5100); Assert.AreEqual(CircuitState.HalfOpen, circuitBreaker.CircuitState); // verify circuit breaker is back open await commonResiliencyPolicy.ExecuteAsync((ctx, ct) => DemoHelper.DemoClient.GetAsync("api/demo/success", ct), new Context($"{nameof(AddResiliencyToHttpCall)}_5"), CancellationToken.None); Assert.AreEqual(CircuitState.Closed, circuitBreaker.CircuitState); }