diff --git a/PollyDemos/Sync/Demo07_WaitAndRetryNestingCircuitBreakerUsingPipeline.cs b/PollyDemos/Sync/Demo07_WaitAndRetryNestingCircuitBreakerUsingPipeline.cs new file mode 100644 index 0000000..5e84e61 --- /dev/null +++ b/PollyDemos/Sync/Demo07_WaitAndRetryNestingCircuitBreakerUsingPipeline.cs @@ -0,0 +1,168 @@ +using System.Diagnostics; +using Polly.CircuitBreaker; +using PollyDemos.OutputHelpers; + +namespace PollyDemos.Sync +{ + /// + /// Demonstrates using the Retry and CircuitBreaker strategies. + /// Same as Demo06 but this time combines the strategies by using ResiliencePipelineBuilder. + /// + /// Loops through a series of HTTP requests, keeping track of each requested + /// item and reporting server failures when encountering exceptions. + /// + /// Observations: + /// The operation is identical to Demo06. + /// The code demonstrates how using the ResiliencePipelineBuilder makes your combined pipeline more concise, at the point of execution. + /// + public class Demo07_WaitAndRetryNestingCircuitBreakerUsingPipeline : SyncDemo + { + private int totalRequests; + private int eventualSuccesses; + private int retries; + private int eventualFailuresDueToCircuitBreaking; + private int eventualFailuresForOtherReasons; + + public override string Description => + "This demonstrates CircuitBreaker (see Demo06), but uses the ResiliencePipelineBuilder to compose the strategies. Only the underlying code differs."; + + public override void Execute(CancellationToken cancellationToken, IProgress progress) + { + ArgumentNullException.ThrowIfNull(progress); + + // Let's call a web API service to make repeated requests to a server. + // The service is programmed to fail after 3 requests in 5 seconds. + + eventualSuccesses = 0; + retries = 0; + eventualFailuresDueToCircuitBreaking = 0; + eventualFailuresForOtherReasons = 0; + totalRequests = 0; + + progress.Report(ProgressWithMessage(nameof(Demo07_WaitAndRetryNestingCircuitBreakerUsingPipeline))); + progress.Report(ProgressWithMessage("======")); + progress.Report(ProgressWithMessage(string.Empty)); + + // New for demo07: here we define a pipeline builder which will be used to compose strategies gradually. + var pipelineBuilder = new ResiliencePipelineBuilder(); + + // New for demo07: the order of strategy definitions has changed. + // Circuit breaker comes first because that will be the inner strategy. + // Retry comes second because that will be the outer strategy. + + // Define our circuit breaker strategy: + pipelineBuilder.AddCircuitBreaker(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + FailureRatio = 1.0, + MinimumThroughput = 4, + BreakDuration = TimeSpan.FromSeconds(3), + OnOpened = args => + { + progress.Report(ProgressWithMessage( + $".Breaker logging: Breaking the circuit for {args.BreakDuration.TotalMilliseconds}ms!", + Color.Magenta)); + + // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred. + // Note the ! sign (null-forgiving operator) at the end of the command. + var exception = args.Outcome.Exception!; //The Exception property is nullable + progress.Report(ProgressWithMessage($"..due to: {exception.Message}", Color.Magenta)); + return default; + }, + OnClosed = args => + { + progress.Report(ProgressWithMessage(".Breaker logging: Call OK! Closed the circuit again!", Color.Magenta)); + return default; + }, + OnHalfOpened = args => + { + progress.Report(ProgressWithMessage(".Breaker logging: Half-open: Next call is a trial!", Color.Magenta)); + return default; + } + }); // New for demo07: here we are not calling the Build method because we want to add one more strategy to the pipeline. + + // Define our retry strategy: + pipelineBuilder.AddRetry(new() + { + // Exception filtering - we don't retry if the inner circuit-breaker judges the underlying system is out of commission. + ShouldHandle = new PredicateBuilder().Handle(ex => ex is not BrokenCircuitException), + MaxRetryAttempts = int.MaxValue, // Retry indefinitely + Delay = TimeSpan.FromMilliseconds(200), // Wait 200ms between each try + OnRetry = args => + { + // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred. + // Note the ! sign (null-forgiving operator) at the end of the command. + var exception = args.Outcome.Exception!; //The Exception property is nullable + + // Tell the user what happened + progress.Report(ProgressWithMessage($"Strategy logging: {exception.Message}", Color.Yellow)); + retries++; + return default; + } + }); // New for demo07: here we are not calling the Build method because we will do it as a separate step to make the code cleaner. + + // New for demo07: here we build the pipeline since we have added all the necessary strategies to it. + var pipeline = pipelineBuilder.Build(); + + var client = new HttpClient(); + var internalCancel = false; + // Do the following until a key is pressed + while (!(internalCancel || cancellationToken.IsCancellationRequested)) + { + totalRequests++; + var watch = Stopwatch.StartNew(); + + try + { + // Manage the call according to the pipeline. + var response = pipeline.Execute(ct => + { + // This code is executed through both strategies in the pipeline: + // Retry is the outer, and circuit breaker is the inner. + // Demo 06 shows a broken-out version of what this is equivalent to. + + // Make a request and get a response + var url = $"{Configuration.WEB_API_ROOT}/api/values/{totalRequests}"; + var response = client.Send(new HttpRequestMessage(HttpMethod.Get, url), ct); + + using var stream = response.Content.ReadAsStream(ct); + using var streamReader = new StreamReader(stream); + return streamReader.ReadToEnd(); + }, cancellationToken); + + watch.Stop(); + + // Display the response message on the console + progress.Report(ProgressWithMessage($"Response : {response} (after {watch.ElapsedMilliseconds}ms)", Color.Green)); + eventualSuccesses++; + } + catch (BrokenCircuitException bce) + { + watch.Stop(); + var logMessage = $"Request {totalRequests} failed with: {bce.GetType().Name} (after {watch.ElapsedMilliseconds}ms)"; + progress.Report(ProgressWithMessage(logMessage, Color.Red)); + eventualFailuresDueToCircuitBreaking++; + } + catch (Exception e) + { + watch.Stop(); + var logMessage = $"Request {totalRequests} eventually failed with: {e.Message} (after {watch.ElapsedMilliseconds}ms)"; + progress.Report(ProgressWithMessage(logMessage, Color.Red)); + eventualFailuresForOtherReasons++; + } + + Thread.Sleep(500); + internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; + } + } + + public override Statistic[] LatestStatistics => new Statistic[] + { + new("Total requests made", totalRequests), + new("Requests which eventually succeeded", eventualSuccesses, Color.Green), + new("Retries made to help achieve success", retries, Color.Yellow), + new("Requests failed early by broken circuit", eventualFailuresDueToCircuitBreaking, Color.Magenta), + new("Requests which failed after longer delay", eventualFailuresForOtherReasons, Color.Red), + }; + } +} diff --git a/PollyDemos/Sync/Demo07_WaitAndRetryNestingCircuitBreakerUsingPolicyWrap.cs b/PollyDemos/Sync/Demo07_WaitAndRetryNestingCircuitBreakerUsingPolicyWrap.cs deleted file mode 100644 index 4043fac..0000000 --- a/PollyDemos/Sync/Demo07_WaitAndRetryNestingCircuitBreakerUsingPolicyWrap.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Diagnostics; -using System.Net; -using Polly.CircuitBreaker; -using PollyDemos.OutputHelpers; - -namespace PollyDemos.Sync -{ - /// - /// Demonstrates using the WaitAndRetry policy nesting CircuitBreaker. - /// Same as Demo06 - but this time demonstrates combining the policies using PolicyWrap. - /// - /// Loops through a series of Http requests, keeping track of each requested - /// item and reporting server failures when encountering exceptions. - /// - /// Obervations from this demo: - /// The operation is identical to Demo06. - /// The code demonstrates how using the PolicyWrap makes your combined-Policy-strategy more concise, at the point of execution. - /// - public class Demo07_WaitAndRetryNestingCircuitBreakerUsingPolicyWrap : SyncDemo - { - private int totalRequests; - private int eventualSuccesses; - private int retries; - private int eventualFailuresDueToCircuitBreaking; - private int eventualFailuresForOtherReasons; - - public override string Description => - "This demonstrates CircuitBreaker (see Demo06), but uses the PolicyWrap syntax to compose the policies nicely. Only the underlying code differs."; - - public override void Execute(CancellationToken cancellationToken, IProgress progress) - { - if (progress == null) throw new ArgumentNullException(nameof(progress)); - - // Let's call a web api service to make repeated requests to a server. - // The service is programmed to fail after 3 requests in 5 seconds. - - eventualSuccesses = 0; - retries = 0; - eventualFailuresDueToCircuitBreaking = 0; - eventualFailuresForOtherReasons = 0; - - progress.Report(ProgressWithMessage(nameof(Demo07_WaitAndRetryNestingCircuitBreakerUsingPolicyWrap))); - progress.Report(ProgressWithMessage("======")); - progress.Report(ProgressWithMessage(string.Empty)); - - // Define our waitAndRetry policy: keep retrying with 200ms gaps. - var waitAndRetryPolicy = Policy - .Handle(e => - !(e is BrokenCircuitException)) // Exception filtering! We don't retry if the inner circuit-breaker judges the underlying system is out of commission! - .WaitAndRetryForever( - attempt => TimeSpan.FromMilliseconds(200), - (exception, calculatedWaitDuration) => - { - // This is your new exception handler! - // Tell the user what they've won! - progress.Report(ProgressWithMessage(".Log,then retry: " + exception.Message, Color.Yellow)); - retries++; - }); - - // Define our CircuitBreaker policy: Break if the action fails 4 times in a row. - var circuitBreakerPolicy = Policy - .Handle() - .CircuitBreaker( - 4, - TimeSpan.FromSeconds(3), - (ex, breakDelay) => - { - progress.Report(ProgressWithMessage( - ".Breaker logging: Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms!", - Color.Magenta)); - progress.Report(ProgressWithMessage("..due to: " + ex.Message, Color.Magenta)); - }, - () => progress.Report(ProgressWithMessage(".Breaker logging: Call ok! Closed the circuit again!", - Color.Magenta)), - () => progress.Report(ProgressWithMessage(".Breaker logging: Half-open: Next call is a trial!", - Color.Magenta)) - ); - - // New for demo07: combine the waitAndRetryPolicy and circuitBreakerPolicy into a PolicyWrap. - var policyWrap = Policy.Wrap(waitAndRetryPolicy, circuitBreakerPolicy); - - using (var client = new WebClient()) - { - var internalCancel = false; - totalRequests = 0; - // Do the following until a key is pressed - while (!internalCancel && !cancellationToken.IsCancellationRequested) - { - totalRequests++; - var watch = new Stopwatch(); - watch.Start(); - - try - { - // Retry the following call according to the policy wrap - var response = policyWrap.Execute( - ct => // The Execute() overload takes a CancellationToken, but it happens the executed code does not honour it. - { - // This code is executed through both policies in the wrap: WaitAndRetry outer, then CircuitBreaker inner. Demo 06 shows a broken-out version of what this is equivalent to. - - return client.DownloadString( - Configuration.WEB_API_ROOT + "/api/values/" + totalRequests); - } - , cancellationToken // The cancellationToken passed in to Execute() enables the policy instance to cancel retries, when the token is signalled. - ); - - // Without the extra comments in the anonymous method { } above, it could even be as concise as this: - // string msg = policyWrap.Execute(() => client.DownloadString(Configuration.WEB_API_ROOT + "/api/values/" + i)); - - watch.Stop(); - - // Display the response message on the console - progress.Report(ProgressWithMessage("Response : " + response - + " (after " + watch.ElapsedMilliseconds + - "ms)", Color.Green)); - - eventualSuccesses++; - } - catch (BrokenCircuitException b) - { - watch.Stop(); - - progress.Report(ProgressWithMessage("Request " + totalRequests + " failed with: " + - b.GetType().Name - + " (after " + watch.ElapsedMilliseconds + "ms)", - Color.Red)); - - eventualFailuresDueToCircuitBreaking++; - } - catch (Exception e) - { - watch.Stop(); - - progress.Report(ProgressWithMessage("Request " + totalRequests + " eventually failed with: " + - e.Message - + " (after " + watch.ElapsedMilliseconds + "ms)", - Color.Red)); - - eventualFailuresForOtherReasons++; - } - - // Wait half second - Thread.Sleep(500); - - internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; - } - } - } - - public override Statistic[] LatestStatistics => new[] - { - new Statistic("Total requests made", totalRequests), - new Statistic("Requests which eventually succeeded", eventualSuccesses, Color.Green), - new Statistic("Retries made to help achieve success", retries, Color.Yellow), - new Statistic("Requests failed early by broken circuit", eventualFailuresDueToCircuitBreaking, - Color.Magenta), - new Statistic("Requests which failed after longer delay", eventualFailuresForOtherReasons, Color.Red), - }; - } -} \ No newline at end of file diff --git a/PollyDemos/Sync/Demo08_Pipeline-Fallback-WaitAndRetry-CircuitBreaker.cs b/PollyDemos/Sync/Demo08_Pipeline-Fallback-WaitAndRetry-CircuitBreaker.cs new file mode 100644 index 0000000..b3ee56e --- /dev/null +++ b/PollyDemos/Sync/Demo08_Pipeline-Fallback-WaitAndRetry-CircuitBreaker.cs @@ -0,0 +1,202 @@ +using System.Diagnostics; +using Polly.CircuitBreaker; +using PollyDemos.OutputHelpers; + +namespace PollyDemos.Sync +{ + /// + /// Demonstrates using a Retry, a CircuitBreaker and two Fallback strategies. + /// Same as Demo07 but now uses Fallback strategies to provide substitute values, when the call still fails overall. + /// + /// Loops through a series of HTTP requests, keeping track of each requested + /// item and reporting server failures when encountering exceptions. + /// + /// Observations: + /// - operation is identical to Demo06 and Demo07 + /// - except fallback strategies provide nice substitute messages, if still fails overall + /// - OnFallback delegate captures the stats that were captured in try/catches in demos 06 and 07 + /// - also demonstrates how you can use the same kind of strategy (Fallback in this case) twice (or more) in a pipeline. + /// + public class Demo08_Pipeline_Fallback_WaitAndRetry_CircuitBreaker : SyncDemo + { + private int totalRequests; + private int eventualSuccesses; + private int retries; + private int eventualFailuresDueToCircuitBreaking; + private int eventualFailuresForOtherReasons; + + public override string Description => + "This demo matches 06 and 07 (retry with circuit-breaker), but also introduces Fallbacks: we can provide graceful fallback messages, on overall failure."; + + public override void Execute(CancellationToken cancellationToken, IProgress progress) + { + ArgumentNullException.ThrowIfNull(progress); + + // Let's call a web API service to make repeated requests to a server. + // The service is programmed to fail after 3 requests in 5 seconds. + + eventualSuccesses = 0; + retries = 0; + eventualFailuresDueToCircuitBreaking = 0; + eventualFailuresForOtherReasons = 0; + totalRequests = 0; + + progress.Report(ProgressWithMessage(nameof(Demo08_Pipeline_Fallback_WaitAndRetry_CircuitBreaker))); + progress.Report(ProgressWithMessage("======")); + progress.Report(ProgressWithMessage(string.Empty)); + + Stopwatch? watch = null; + + // New for demo08: we had to provide the return type (string) to be able to use Fallback. + var pipelineBuilder = new ResiliencePipelineBuilder(); + + // Define our circuit breaker strategy: + pipelineBuilder.AddCircuitBreaker(new() + { + // New for demo08: since pipeline is aware of the return type that's why the PredicateBuilder has to be as well. + ShouldHandle = new PredicateBuilder().Handle(), + FailureRatio = 1.0, + MinimumThroughput = 4, + BreakDuration = TimeSpan.FromSeconds(3), + OnOpened = args => + { + progress.Report(ProgressWithMessage( + $".Breaker logging: Breaking the circuit for {args.BreakDuration.TotalMilliseconds}ms!", + Color.Magenta)); + + // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred. + // Note the ! sign (null-forgiving operator) at the end of the command. + var exception = args.Outcome.Exception!; //The Exception property is nullable + progress.Report(ProgressWithMessage($"..due to: {exception.Message}", Color.Magenta)); + return default; + }, + OnClosed = args => + { + progress.Report(ProgressWithMessage(".Breaker logging: Call OK! Closed the circuit again!", Color.Magenta)); + return default; + }, + OnHalfOpened = args => + { + progress.Report(ProgressWithMessage(".Breaker logging: Half-open: Next call is a trial!", Color.Magenta)); + return default; + } + }); + + // Define our retry strategy: + pipelineBuilder.AddRetry(new() + { + // New for demo08: since pipeline is aware of the return type that's why the PredicateBuilder has to be as well. + // Exception filtering - we don't retry if the inner circuit-breaker judges the underlying system is out of commission. + ShouldHandle = new PredicateBuilder().Handle(ex => ex is not BrokenCircuitException), + MaxRetryAttempts = int.MaxValue, // Retry indefinitely + Delay = TimeSpan.FromMilliseconds(200), // Wait 200ms between each try + OnRetry = args => + { + // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred. + // Note the ! sign (null-forgiving operator) at the end of the command. + var exception = args.Outcome.Exception!; //The Exception property is nullable + + // Tell the user what happened + progress.Report(ProgressWithMessage($"Strategy logging: {exception.Message}", Color.Yellow)); + retries++; + return default; + } + }); + + // Define a fallback strategy: provide a substitute message to the user, if we found the circuit was broken. + pipelineBuilder.AddFallback(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + FallbackAction = args => Outcome.FromResultAsValueTask("Please try again later [message substituted by fallback strategy]"), + OnFallback = args => + { + watch!.Stop(); + + // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred. + // Note the ! sign (null-forgiving operator) at the end of the command. + var exception = args.Outcome.Exception!; //The Exception property is nullable + + progress.Report(ProgressWithMessage($"Fallback catches failed with: {exception.Message} (after {watch.ElapsedMilliseconds}ms)", Color.Red)); + eventualFailuresDueToCircuitBreaking++; + return default; + } + }); + + // Define a fallback strategy: provide a substitute message to the user, for any exception. + pipelineBuilder.AddFallback(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + FallbackAction = args => Outcome.FromResultAsValueTask("Please try again later [Fallback for any exception]"), + OnFallback = args => + { + watch!.Stop(); + + // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred. + // Note the ! sign (null-forgiving operator) at the end of the command. + var exception = args.Outcome.Exception!; //The Exception property is nullable + + progress.Report(ProgressWithMessage($"Fallback catches eventually failed with: {exception.Message} (after {watch.ElapsedMilliseconds}ms)", Color.Red)); + + eventualFailuresForOtherReasons++; + return default; + } + }); + + // Build the pipeline which now composes four strategies (from inner to outer): + // Circuit Breaker + // Retry + // Fallback for open circuit + // Fallback for any other exception + var pipeline = pipelineBuilder.Build(); + + var client = new HttpClient(); + var internalCancel = false; + // Do the following until a key is pressed + while (!(internalCancel || cancellationToken.IsCancellationRequested)) + { + totalRequests++; + watch = Stopwatch.StartNew(); + + try + { + // Manage the call according to the pipeline. + var response = pipeline.Execute(ct => + { + // Make a request and get a response + var url = $"{Configuration.WEB_API_ROOT}/api/values/{totalRequests}"; + var response = client.Send(new HttpRequestMessage(HttpMethod.Get, url), ct); + + using var stream = response.Content.ReadAsStream(ct); + using var streamReader = new StreamReader(stream); + return streamReader.ReadToEnd(); + }, cancellationToken); + + watch.Stop(); + + // Display the response message on the console + progress.Report(ProgressWithMessage($"Response : {response} (after {watch.ElapsedMilliseconds}ms)", Color.Green)); + eventualSuccesses++; + } + // This try-catch is not needed, since we have a Fallback for any Exceptions. + // It's only been left in to *demonstrate* it should never get hit. + catch (Exception e) + { + var errorMessage = "Should never arrive here. Use of fallback for any Exception should have provided nice fallback value for exceptions."; + throw new InvalidOperationException(errorMessage, e); + } + + Thread.Sleep(500); + internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; + } + } + + public override Statistic[] LatestStatistics => new Statistic[] + { + new("Total requests made", totalRequests), + new("Requests which eventually succeeded", eventualSuccesses, Color.Green), + new("Retries made to help achieve success", retries, Color.Yellow), + new("Requests failed early by broken circuit", eventualFailuresDueToCircuitBreaking, Color.Magenta), + new("Requests which failed after longer delay", eventualFailuresForOtherReasons, Color.Red), + }; + } +} diff --git a/PollyDemos/Sync/Demo08_Wrap-Fallback-WaitAndRetry-CircuitBreaker.cs b/PollyDemos/Sync/Demo08_Wrap-Fallback-WaitAndRetry-CircuitBreaker.cs deleted file mode 100644 index 7c82c4a..0000000 --- a/PollyDemos/Sync/Demo08_Wrap-Fallback-WaitAndRetry-CircuitBreaker.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System.Diagnostics; -using System.Net; -using Polly.CircuitBreaker; -using PollyDemos.OutputHelpers; - -namespace PollyDemos.Sync -{ - /// - /// Demonstrates a PolicyWrap including two Fallback policies (for different exceptions), WaitAndRetry and CircuitBreaker. - /// As Demo07 - but now uses Fallback policies to provide substitute values, when the call still fails overall. - /// - /// Loops through a series of Http requests, keeping track of each requested - /// item and reporting server failures when encountering exceptions. - /// - /// Obervations from this demo: - /// - operation identical to Demo06 and Demo07 - /// - except fallback policies provide nice substitute messages, if still fails overall - /// - onFallback delegate captures the stats that were captured in try/catches in demos 06 and 07 - /// - also demonstrates how you can use the same kind of policy (Fallback in this case) twice (or more) in a wrap. - /// - public class Demo08_Wrap_Fallback_WaitAndRetry_CircuitBreaker : SyncDemo - { - private int totalRequests; - private int eventualSuccesses; - private int retries; - private int eventualFailuresDueToCircuitBreaking; - private int eventualFailuresForOtherReasons; - - public override string Description => - "This demo matches 06 and 07 (retry with circuit-breaker), but also introduces a Fallback: we can provide a graceful fallback message, on overall failure."; - - public override void Execute(CancellationToken cancellationToken, IProgress progress) - { - if (progress == null) throw new ArgumentNullException(nameof(progress)); - - // Let's call a web api service to make repeated requests to a server. - // The service is programmed to fail after 3 requests in 5 seconds. - - eventualSuccesses = 0; - retries = 0; - eventualFailuresDueToCircuitBreaking = 0; - eventualFailuresForOtherReasons = 0; - - progress.Report(ProgressWithMessage(nameof(Demo08_Wrap_Fallback_WaitAndRetry_CircuitBreaker))); - progress.Report(ProgressWithMessage("======")); - progress.Report(ProgressWithMessage(string.Empty)); - - Stopwatch watch = null; - - // Define our waitAndRetry policy: keep retrying with 200ms gaps. - var waitAndRetryPolicy = Policy - .Handle(e => - !(e is BrokenCircuitException)) // Exception filtering! We don't retry if the inner circuit-breaker judges the underlying system is out of commission! - .WaitAndRetryForever( - attempt => TimeSpan.FromMilliseconds(200), - (exception, calculatedWaitDuration) => - { - progress.Report(ProgressWithMessage(".Log,then retry: " + exception.Message, Color.Yellow)); - retries++; - }); - - // Define our CircuitBreaker policy: Break if the action fails 4 times in a row. - var circuitBreakerPolicy = Policy - .Handle() - .CircuitBreaker( - 4, - TimeSpan.FromSeconds(3), - (ex, breakDelay) => - { - progress.Report(ProgressWithMessage( - ".Breaker logging: Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms!", - Color.Magenta)); - progress.Report(ProgressWithMessage("..due to: " + ex.Message, Color.Magenta)); - }, - () => progress.Report(ProgressWithMessage(".Breaker logging: Call ok! Closed the circuit again!", - Color.Magenta)), - () => progress.Report(ProgressWithMessage(".Breaker logging: Half-open: Next call is a trial!", - Color.Magenta)) - ); - - - // Define a fallback policy: provide a nice substitute message to the user, if we found the circuit was broken. - var fallbackForCircuitBreaker = Policy - .Handle() - .Fallback( - /* Demonstrates fallback value syntax */ - "Please try again later [message substituted by fallback policy]", - b => - { - watch.Stop(); - - progress.Report(ProgressWithMessage("Fallback catches failed with: " + b.Exception.Message - + " (after " + - watch.ElapsedMilliseconds + - "ms)", Color.Red)); - - eventualFailuresDueToCircuitBreaking++; - } - ); - - // Define a fallback policy: provide a substitute string to the user, for any exception. - var fallbackForAnyException = Policy - .Handle() - .Fallback( - /* Demonstrates fallback action/func syntax */ () => - { - return "Please try again later [Fallback for any exception]"; - }, - e => - { - watch.Stop(); - - progress.Report(ProgressWithMessage( - "Fallback catches eventually failed with: " + e.Exception.Message - + " (after " + watch.ElapsedMilliseconds + - "ms)", Color.Red)); - - eventualFailuresForOtherReasons++; - } - ); - - - // As demo07: we combine the waitAndRetryPolicy and circuitBreakerPolicy into a PolicyWrap, using the *static* Policy.Wrap syntax. - var myResilienceStrategy = Policy.Wrap(waitAndRetryPolicy, circuitBreakerPolicy); - - // Added in demo08: we wrap the two fallback policies onto the front of the existing wrap too. Demonstrates the *instance* wrap syntax. And the fact that the PolicyWrap myResilienceStrategy from above is just another Policy, which can be onward-wrapped too. - // With this pattern, you can build an overall resilience strategy programmatically, reusing some common parts (eg PolicyWrap myResilienceStrategy) but varying other parts (eg Fallback) individually for different calls. - var policyWrap = fallbackForAnyException.Wrap(fallbackForCircuitBreaker.Wrap(myResilienceStrategy)); - // For info: Equivalent to: PolicyWrap policyWrap = Policy.Wrap(fallbackForAnyException, fallbackForCircuitBreaker, waitAndRetryPolicy, circuitBreakerPolicy); - - using (var client = new WebClient()) - { - var internalCancel = false; - totalRequests = 0; - // Do the following until a key is pressed - while (!internalCancel && !cancellationToken.IsCancellationRequested) - { - totalRequests++; - watch = new Stopwatch(); - watch.Start(); - - try - { - // Manage the call according to the whole policy wrap. - var response = - policyWrap.Execute( - ct => client.DownloadString(Configuration.WEB_API_ROOT + "/api/values/" + - totalRequests), cancellationToken); - - watch.Stop(); - - // Display the response message on the console - progress.Report(ProgressWithMessage( - "Response : " + response + " (after " + watch.ElapsedMilliseconds + "ms)", Color.Green)); - - eventualSuccesses++; - } - catch (Exception e - ) // try-catch not needed, now that we have a Fallback.Handle. It's only been left in to *demonstrate* it should never get hit. - { - throw new InvalidOperationException( - "Should never arrive here. Use of fallbackForAnyException should have provided nice fallback value for any exceptions.", - e); - } - - // Wait half second - Thread.Sleep(500); - - internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; - } - } - } - - public override Statistic[] LatestStatistics => new[] - { - new Statistic("Total requests made", totalRequests), - new Statistic("Requests which eventually succeeded", eventualSuccesses, Color.Green), - new Statistic("Retries made to help achieve success", retries, Color.Yellow), - new Statistic("Requests failed early by broken circuit", eventualFailuresDueToCircuitBreaking, - Color.Magenta), - new Statistic("Requests which failed after longer delay", eventualFailuresForOtherReasons, Color.Red), - }; - } -} \ No newline at end of file diff --git a/PollyDemos/Sync/Demo09_Pipeline-Fallback-Timeout-WaitAndRetry.cs b/PollyDemos/Sync/Demo09_Pipeline-Fallback-Timeout-WaitAndRetry.cs new file mode 100644 index 0000000..f4eaab5 --- /dev/null +++ b/PollyDemos/Sync/Demo09_Pipeline-Fallback-Timeout-WaitAndRetry.cs @@ -0,0 +1,176 @@ +using System.Diagnostics; +using Polly.Timeout; +using PollyDemos.OutputHelpers; + +namespace PollyDemos.Sync +{ + /// + /// Demonstrates using a Retry, a Timeout and two Fallback strategies. + /// In this demo, the delay in the retry is deliberately so long that the timeout wrapping it will time it out + /// (in lieu for now of a demo server endpoint responding slowly). + /// + /// Loops through a series of HTTP requests, keeping track of each requested + /// item and reporting server failures when encountering exceptions. + /// + /// Observations: + /// - though the console logs that a retry will be made, the 4-second wait before the retry is pre-emptively timed-out by the two-second timeout + /// - a fallback strategy then provides substitute message for the user + /// - otherwise similar to demo08. + /// + public class Demo09_Pipeline_Fallback_Timeout_WaitAndRetry : SyncDemo + { + private int totalRequests; + private int eventualSuccesses; + private int retries; + private int eventualFailuresDueToTimeout; + private int eventualFailuresForOtherReasons; + + public override string Description => + "Demonstrates introducing a Timeout strategy. The timeout will eventually time-out on the retries. When we timeout, we again use a Fallback to substitute a more graceful message."; + + public override void Execute(CancellationToken cancellationToken, IProgress progress) + { + ArgumentNullException.ThrowIfNull(progress); + + // Let's call a web API service to make repeated requests to a server. + // The service is programmed to fail after 3 requests in 5 seconds. + + eventualSuccesses = 0; + retries = 0; + eventualFailuresDueToTimeout = 0; + eventualFailuresForOtherReasons = 0; + totalRequests = 0; + + progress.Report(ProgressWithMessage(nameof(Demo09_Pipeline_Fallback_Timeout_WaitAndRetry))); + progress.Report(ProgressWithMessage("======")); + progress.Report(ProgressWithMessage(string.Empty)); + + Stopwatch? watch = null; + var pipelineBuilder = new ResiliencePipelineBuilder(); + + // Define our timeout strategy: time out after 2 seconds. + pipelineBuilder.AddTimeout(new TimeoutStrategyOptions() + { + Timeout = TimeSpan.FromSeconds(2), + OnTimeout = args => + { + var logMessage = $".The task was terminated because it ran out of time. Time cap was {args.Timeout.TotalSeconds}s"; + progress.Report(ProgressWithMessage(logMessage, Color.Yellow)); + return default; + } + }); + + // Define our retry strategy: keep retrying with 4 second gaps. This is (intentionally) too long: to demonstrate that the timeout strategy will time out on this before waiting for the retry. + pipelineBuilder.AddRetry(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + Delay = TimeSpan.FromSeconds(4), + MaxRetryAttempts = int.MaxValue, + OnRetry = args => + { + // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred. + // Note the ! sign (null-forgiving operator) at the end of the command. + var exception = args.Outcome.Exception!; //The Exception property is nullable + progress.Report(ProgressWithMessage($".Log,then retry: {exception.Message}", Color.Yellow)); + retries++; + return default; + } + }); + + // Define a fallback strategy: provide a substitute message to the user, if we found the call was rejected due to timeout. + pipelineBuilder.AddFallback(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + FallbackAction = args => Outcome.FromResultAsValueTask("Please try again later [Fallback for timeout]"), + OnFallback = args => + { + watch!.Stop(); + + // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred. + // Note the ! sign (null-forgiving operator) at the end of the command. + var exception = args.Outcome.Exception!; //The Exception property is nullable + + progress.Report(ProgressWithMessage($"Fallback catches failed with: {exception.Message} (after {watch.ElapsedMilliseconds}ms)", Color.Red)); + eventualFailuresDueToTimeout++; + return default; + } + }); + + // Define a fallback strategy: provide a substitute message to the user, for any exception. + pipelineBuilder.AddFallback(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + FallbackAction = args => Outcome.FromResultAsValueTask("Please try again later [Fallback for any exception]"), + OnFallback = args => + { + watch!.Stop(); + + // Due to how we have defined ShouldHandle, this delegate is called only if an exception occurred. + // Note the ! sign (null-forgiving operator) at the end of the command. + var exception = args.Outcome.Exception!; //The Exception property is nullable + + progress.Report(ProgressWithMessage($"Fallback catches eventually failed with: {exception.Message} (after {watch.ElapsedMilliseconds}ms)", Color.Red)); + + eventualFailuresForOtherReasons++; + return default; + } + }); + + // Build the pipeline which now composes four strategies (from inner to outer): + // Timeout + // Retry + // Fallback for timeout + // Fallback for any other exception + var pipeline = pipelineBuilder.Build(); + + var client = new HttpClient(); + var internalCancel = false; + // Do the following until a key is pressed + while (!(internalCancel || cancellationToken.IsCancellationRequested)) + { + totalRequests++; + watch = Stopwatch.StartNew(); + + try + { + // Manage the call according to the pipeline. + var response = pipeline.Execute(ct => + { + // Make a request and get a response + var url = $"{Configuration.WEB_API_ROOT}/api/values/{totalRequests}"; + var response = client.Send(new HttpRequestMessage(HttpMethod.Get, url), ct); + + using var stream = response.Content.ReadAsStream(ct); + using var streamReader = new StreamReader(stream); + return streamReader.ReadToEnd(); + }, cancellationToken); + + watch.Stop(); + + // Display the response message on the console + progress.Report(ProgressWithMessage($"Response : {response} (after {watch.ElapsedMilliseconds}ms)", Color.Green)); + eventualSuccesses++; + } + // This try-catch is not needed, since we have a Fallback for any Exceptions. + // It's only been left in to *demonstrate* it should never get hit. + catch (Exception e) + { + var errorMessage = "Should never arrive here. Use of fallback for any Exception should have provided nice fallback value for exceptions."; + throw new InvalidOperationException(errorMessage, e); + } + + Thread.Sleep(500); + internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; + } + } + + public override Statistic[] LatestStatistics => new Statistic[] + { + new("Total requests made", totalRequests), + new("Requests which eventually succeeded", eventualSuccesses, Color.Green), + new("Retries made to help achieve success", retries, Color.Yellow), + new("Requests timed out by timeout strategy", eventualFailuresDueToTimeout, Color.Magenta), + new("Requests which failed after longer delay", eventualFailuresForOtherReasons, Color.Red), + }; + } +} diff --git a/PollyDemos/Sync/Demo09_Wrap-Fallback-Timeout-WaitAndRetry.cs b/PollyDemos/Sync/Demo09_Wrap-Fallback-Timeout-WaitAndRetry.cs deleted file mode 100644 index 4786130..0000000 --- a/PollyDemos/Sync/Demo09_Wrap-Fallback-Timeout-WaitAndRetry.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System.Diagnostics; -using System.Net; -using Polly.Timeout; -using PollyDemos.OutputHelpers; - -namespace PollyDemos.Sync -{ - /// - /// Demonstrates a PolicyWrap including Fallback, Timeout and WaitAndRetry. - /// In this demo, the wait in the wait-and-retry is deliberately so long that the timeout policy wrapping it will time it out - /// (in lieu for now of a demo server endpoint responding slowly). - /// - /// Loops through a series of Http requests, keeping track of each requested - /// item and reporting server failures when encountering exceptions. - /// - /// Obervations from this demo: - /// - though the console logs that a retry will be made, the 4-second wait before the retry is pre-emptively timed-out by the two-second timeout - /// - a fallback policy then provides substitute message for the user - /// - otherwise similar to demo08. - /// - public class Demo09_Wrap_Fallback_Timeout_WaitAndRetry : SyncDemo - { - private int totalRequests; - private int eventualSuccesses; - private int retries; - private int eventualFailuresDueToTimeout; - private int eventualFailuresForOtherReasons; - - public override string Description => - "Demonstrates introducing a TimeoutPolicy. The TimeoutPolicy will eventually time-out on the retries that WaitAndRetry was orchestrating. When we timeout, we again use a Fallback policy to substitute a more graceful message."; - - public override void Execute(CancellationToken cancellationToken, IProgress progress) - { - if (progress == null) throw new ArgumentNullException(nameof(progress)); - - // Let's call a web api service to make repeated requests to a server. - // The service is programmed to fail after 3 requests in 5 seconds. - - eventualSuccesses = 0; - retries = 0; - eventualFailuresDueToTimeout = 0; - eventualFailuresForOtherReasons = 0; - - progress.Report(ProgressWithMessage(nameof(Demo09_Wrap_Fallback_Timeout_WaitAndRetry))); - progress.Report(ProgressWithMessage("======")); - progress.Report(ProgressWithMessage(string.Empty)); - - Stopwatch watch = null; - - // Define our timeout policy: time out after 2 seconds. We will use the pessimistic timeout strategy, which forces a timeout - even when the underlying delegate doesn't support it. - var timeoutPolicy = Policy - .Timeout(TimeSpan.FromSeconds(2), TimeoutStrategy.Pessimistic, - // This use of onTimeout demonstrates the point about capturing walked-away-from Tasks with TimeoutStrategy.Pessimistic discussed in the Polly wiki, here: https://github.com/App-vNext/Polly/wiki/Timeout#pessimistic-timeout-1 - (ctx, span, abandonedTask) => - { - { - abandonedTask.ContinueWith(t => - { - // ContinueWith important!: the abandoned task may very well still be executing, when the caller times out on waiting for it! - - if (t.IsFaulted) - progress.Report(ProgressWithMessage( - ".The task previously walked-away-from now terminated with exception: " + - t.Exception.Message, - Color.Yellow)); - else if (t.IsCanceled) - // (If the executed delegates do not honour cancellation, this IsCanceled branch may never be hit. It can be good practice however to include, in case a Policy configured with TimeoutStrategy.Pessimistic is used to execute a delegate honouring cancellation.) - progress.Report(ProgressWithMessage( - ".The task previously walked-away-from now was canceled.", Color.Yellow)); - else - // extra logic (if desired) for tasks which complete, despite the caller having 'walked away' earlier due to timeout. - progress.Report(ProgressWithMessage( - ".The task previously walked-away-from now eventually completed.", - Color.Yellow)); - }); - } - } - ); - - // Define our waitAndRetry policy: keep retrying with 4 second gaps. This is (intentionally) too long: to demonstrate that the timeout policy will time out on this before waiting for the retry. - var waitAndRetryPolicy = Policy - .Handle() - .WaitAndRetryForever( - attempt => TimeSpan.FromSeconds(4), - (exception, calculatedWaitDuration) => - { - progress.Report(ProgressWithMessage(".Log,then retry: " + exception.Message, Color.Yellow)); - retries++; - }); - - // Define a fallback policy: provide a nice substitute message to the user, if we found the call was rejected due to the timeout policy. - var fallbackForTimeout = Policy - .Handle() - .Fallback( - /* Demonstrates fallback value syntax */ "Please try again later [Fallback for timeout]", - b => - { - watch.Stop(); - progress.Report(ProgressWithMessage( - "Fallback catches failed with: " + b.Exception.Message + " (after " + - watch.ElapsedMilliseconds + "ms)", Color.Red)); - eventualFailuresDueToTimeout++; - } - ); - - // Define a fallback policy: provide a substitute string to the user, for any exception. - var fallbackForAnyException = Policy - .Handle() - .Fallback( - /* Demonstrates fallback action/func syntax */ () => - { - return "Please try again later [Fallback for any exception]"; - }, - e => - { - watch.Stop(); - - progress.Report(ProgressWithMessage( - "Fallback catches eventually failed with: " + e.Exception.Message + " (after " + - watch.ElapsedMilliseconds + "ms)", Color.Red)); - - eventualFailuresForOtherReasons++; - } - ); - - - // Compared to previous demo08: here we use *instance* wrap syntax, to wrap all in one go. - var policyWrap = fallbackForAnyException.Wrap(fallbackForTimeout).Wrap(timeoutPolicy) - .Wrap(waitAndRetryPolicy); - - using (var client = new WebClient()) - { - var internalCancel = false; - totalRequests = 0; - while (!internalCancel && !cancellationToken.IsCancellationRequested) - { - totalRequests++; - watch = new Stopwatch(); - watch.Start(); - - try - { - // Manage the call according to the whole policy wrap. - var response = - policyWrap.Execute( - ct => client.DownloadString(Configuration.WEB_API_ROOT + "/api/values/" + - totalRequests), cancellationToken); - - watch.Stop(); - - progress.Report(ProgressWithMessage( - "Response: " + response + "(after " + watch.ElapsedMilliseconds + "ms)", Color.Green)); - - eventualSuccesses++; - } - catch (Exception e - ) // try-catch not needed, now that we have a Fallback.Handle. It's only been left in to *demonstrate* it should never get hit. - { - throw new InvalidOperationException( - "Should never arrive here. Use of fallbackForAnyException should have provided nice fallback value for any exceptions.", - e); - } - - // Wait half second - Thread.Sleep(500); - - internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; - } - } - } - - public override Statistic[] LatestStatistics => new[] - { - new Statistic("Total requests made", totalRequests), - new Statistic("Requests which eventually succeeded", eventualSuccesses, Color.Green), - new Statistic("Retries made to help achieve success", retries, Color.Yellow), - new Statistic("Requests timed out by timeout policy", eventualFailuresDueToTimeout, Color.Magenta), - new Statistic("Requests which failed after longer delay", eventualFailuresForOtherReasons, Color.Red), - }; - } -} \ No newline at end of file