diff --git a/PollyDemos/Sync/Demo00_NoPolicy.cs b/PollyDemos/Sync/Demo00_NoStrategy.cs similarity index 91% rename from PollyDemos/Sync/Demo00_NoPolicy.cs rename to PollyDemos/Sync/Demo00_NoStrategy.cs index 559901b..a40bbe2 100644 --- a/PollyDemos/Sync/Demo00_NoPolicy.cs +++ b/PollyDemos/Sync/Demo00_NoStrategy.cs @@ -3,11 +3,11 @@ namespace PollyDemos.Sync { /// - /// Uses no policy. Demonstrates behaviour of 'faulting server' we are testing against. + /// Uses no strategy. Demonstrates behavior of 'faulting server' we are testing against. /// Loops through a series of Http requests, keeping track of each requested /// item and reporting server failures when encountering exceptions. /// - public class Demo00_NoPolicy : SyncDemo + public class Demo00_NoStrategy : SyncDemo { private int totalRequests; private int eventualSuccesses; @@ -22,7 +22,12 @@ public override void Execute(CancellationToken cancellationToken, IProgress - /// Demonstrates the Retry policy coming into action. + /// Demonstrates the Retry strategy coming into action. /// Loops through a series of Http requests, keeping track of each requested /// item and reporting server failures when encountering exceptions. - /// - /// Observations: There's no wait among these retries. Can be appropriate sometimes. + /// + /// Observations: There's no wait among these retries. Can be appropriate sometimes. /// In this case, no wait hasn't given underlying system time to recover, so calls still fail despite retries. /// public class Demo01_RetryNTimes : SyncDemo @@ -23,80 +22,85 @@ public class Demo01_RetryNTimes : SyncDemo public override void Execute(CancellationToken cancellationToken, IProgress progress) { - if (progress == null) throw new ArgumentNullException(nameof(progress)); + ArgumentNullException.ThrowIfNull(progress); - // Let's call a web api service to make repeated requests to a server. + // 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; eventualFailures = 0; + totalRequests = 0; progress.Report(ProgressWithMessage(nameof(Demo01_RetryNTimes))); progress.Report(ProgressWithMessage("======")); progress.Report(ProgressWithMessage(string.Empty)); - // Let's call a web api service to make repeated requests to a server. + // 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. - // Define our policy: - var policy = Policy.Handle().Retry(3, (exception, attempt) => + // Define our strategy: + var strategy = new ResiliencePipelineBuilder().AddRetry(new() { - // This is your new exception handler! - // Tell the user what they've won! - progress.Report(ProgressWithMessage("Policy logging: " + exception.Message, Color.Yellow)); - retries++; - }); + ShouldHandle = new PredicateBuilder().Handle(), + MaxRetryAttempts = 3, // Retry up to 3 times + 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; + } + }).Build(); - using (var client = new WebClient()) + var client = new HttpClient(); + var internalCancel = false; + // Do the following until a key is pressed + while (!(internalCancel || cancellationToken.IsCancellationRequested)) { - var internalCancel = false; - totalRequests = 0; - // Do the following until a key is pressed - while (!internalCancel && !cancellationToken.IsCancellationRequested) - { - totalRequests++; + totalRequests++; - try - { - // Retry the following call according to the policy - 3 times. - policy.Execute( - ct => // The Execute() overload takes a CancellationToken, but it happens the executed code does not honour it. - { - // This code is executed within the Policy - - // Make a request and get a response - var response = - client.DownloadString(Configuration.WEB_API_ROOT + "/api/values/" + totalRequests); - - // Display the response message on the console - progress.Report(ProgressWithMessage("Response : " + response, Color.Green)); - eventualSuccesses++; - } - , cancellationToken // The cancellationToken passed in to Execute() enables the policy instance to cancel retries, when the token is signalled. - ); - } - catch (Exception e) + try + { + // Retry the following call according to the strategy - 3 times. + // The cancellationToken passed in to Execute() enables the strategy to cancel retries, when the token is signalled. + strategy.Execute(ct => { - progress.Report(ProgressWithMessage( - "Request " + totalRequests + " eventually failed with: " + e.Message, Color.Red)); - eventualFailures++; - } + // This code is executed within the strategy + + // 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); - // Wait half second - Thread.Sleep(500); + // Display the response message on the console + using var stream = response.Content.ReadAsStream(ct); + using var streamReader = new StreamReader(stream); + progress.Report(ProgressWithMessage($"Response : {streamReader.ReadToEnd()}", Color.Green)); + eventualSuccesses++; - internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; + }, cancellationToken); } + catch (Exception e) + { + progress.Report(ProgressWithMessage($"Request {totalRequests} eventually failed with: {e.Message}", Color.Red)); + eventualFailures++; + } + + Thread.Sleep(500); + internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; } } - public override Statistic[] LatestStatistics => new[] + public override Statistic[] LatestStatistics => new Statistic[] { - 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 which eventually failed", eventualFailures, Color.Red), + 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 which eventually failed", eventualFailures, Color.Red), }; } -} \ No newline at end of file +} diff --git a/PollyDemos/Sync/Demo02_WaitAndRetryNTimes.cs b/PollyDemos/Sync/Demo02_WaitAndRetryNTimes.cs index 5ce654a..c2206d0 100644 --- a/PollyDemos/Sync/Demo02_WaitAndRetryNTimes.cs +++ b/PollyDemos/Sync/Demo02_WaitAndRetryNTimes.cs @@ -1,13 +1,12 @@ -using System.Net; -using PollyDemos.OutputHelpers; +using PollyDemos.OutputHelpers; namespace PollyDemos.Sync { /// - /// Demonstrates the WaitAndRetry policy. + /// Demonstrates the Retry strategy with delays between retry attempts. /// Loops through a series of Http requests, keeping track of each requested /// item and reporting server failures when encountering exceptions. - /// + /// /// Observations: We now have waits among the retries. /// In this case, still not enough wait - or not enough retries - for the underlying system to have recovered. /// So we still fail some calls. @@ -20,87 +19,89 @@ public class Demo02_WaitAndRetryNTimes : SyncDemo private int eventualFailures; public override string Description => - "Compared to previous demo, this demo adds waits between the retries. Not always enough wait to ensure success, tho."; + "Compared to previous demo, this demo adds waits between the retry attempts. Not always enough wait to ensure success, tho."; public override void Execute(CancellationToken cancellationToken, IProgress progress) { - if (progress == null) throw new ArgumentNullException(nameof(progress)); + ArgumentNullException.ThrowIfNull(progress); - // Let's call a web api service to make repeated requests to a server. + // 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; eventualFailures = 0; + totalRequests = 0; progress.Report(ProgressWithMessage(nameof(Demo02_WaitAndRetryNTimes))); progress.Report(ProgressWithMessage("======")); progress.Report(ProgressWithMessage(string.Empty)); - // Let's call a web api service to make repeated requests to a server. + // 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. - // Define our policy: - var policy = Policy.Handle().WaitAndRetry( - 3, // Retry 3 times - attempt => TimeSpan.FromMilliseconds(200), // Wait 200ms between each try. - (exception, calculatedWaitDuration) => // Capture some info for logging! - { - // This is your new exception handler! - // Tell the user what they've won! - progress.Report(ProgressWithMessage("Policy logging: " + exception.Message, Color.Yellow)); - retries++; - }); - - using (var client = new WebClient()) + // Define our strategy: + var strategy = new ResiliencePipelineBuilder().AddRetry(new() { - totalRequests = 0; - var internalCancel = false; - // Do the following until a key is pressed - while (!internalCancel && !cancellationToken.IsCancellationRequested) + ShouldHandle = new PredicateBuilder().Handle(), + MaxRetryAttempts = 3, + Delay = TimeSpan.FromMilliseconds(200), // Wait 200ms between each try + OnRetry = args => { - totalRequests++; + // 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 - try - { - // Retry the following call according to the policy - 3 times. - policy.Execute( - ct => // The Execute() overload takes a CancellationToken, but it happens the executed code does not honour it. - { - // This code is executed within the Policy + // Tell the user what happened + progress.Report(ProgressWithMessage("Strategy logging: " + exception.Message, Color.Yellow)); + retries++; + return default; + } + }).Build(); - // Make a request and get a response - var response = - client.DownloadString(Configuration.WEB_API_ROOT + "/api/values/" + totalRequests); + var client = new HttpClient(); + var internalCancel = false; + // Do the following until a key is pressed + while (!(internalCancel || cancellationToken.IsCancellationRequested)) + { + totalRequests++; - // Display the response message on the console - progress.Report(ProgressWithMessage("Response : " + response, Color.Green)); - eventualSuccesses++; - } - , cancellationToken // The cancellationToken passed in to Execute() enables the policy instance to cancel retries, when the token is signalled. - ); - } - catch (Exception e) + try + { + // Retry the following call according to the strategy - 3 times. + // The cancellationToken passed in to Execute() enables the strategy to cancel retries, when the token is signalled. + strategy.Execute(ct => { - progress.Report(ProgressWithMessage( - "Request " + totalRequests + " eventually failed with: " + e.Message, Color.Red)); - eventualFailures++; - } + // This code is executed within the strategy - // Wait half second - Thread.Sleep(500); + // 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); - internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; + // Display the response message on the console + using var stream = response.Content.ReadAsStream(ct); + using var streamReader = new StreamReader(stream); + progress.Report(ProgressWithMessage($"Response : {streamReader.ReadToEnd()}", Color.Green)); + eventualSuccesses++; + }, cancellationToken); } + catch (Exception e) + { + progress.Report(ProgressWithMessage($"Request {totalRequests} eventually failed with: {e.Message}", Color.Red)); + eventualFailures++; + } + + Thread.Sleep(500); + internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; } } - public override Statistic[] LatestStatistics => new[] + public override Statistic[] LatestStatistics => new Statistic[] { - 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 which eventually failed", eventualFailures, Color.Red), + 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 which eventually failed", eventualFailures, Color.Red), }; } -} \ No newline at end of file +} diff --git a/PollyDemos/Sync/Demo03_WaitAndRetryNTimes_WithEnoughRetries.cs b/PollyDemos/Sync/Demo03_WaitAndRetryNTimes_WithEnoughRetries.cs index 77caaf7..4a53d80 100644 --- a/PollyDemos/Sync/Demo03_WaitAndRetryNTimes_WithEnoughRetries.cs +++ b/PollyDemos/Sync/Demo03_WaitAndRetryNTimes_WithEnoughRetries.cs @@ -1,13 +1,12 @@ -using System.Net; -using PollyDemos.OutputHelpers; +using PollyDemos.OutputHelpers; namespace PollyDemos.Sync { /// - /// Demonstrates the WaitAndRetry policy. + /// Demonstrates the Retry strategy with delays between retry attempts. /// Loops through a series of Http requests, keeping track of each requested /// item and reporting server failures when encountering exceptions. - /// + /// /// Observations: We now have waits and enough retries: all calls now succeed! Yay! /// But we kind-a had to guess how many retries would be enough before the server responded again ... /// (and we're hammering that server with retries) @@ -24,84 +23,87 @@ public class Demo03_WaitAndRetryNTimes_WithEnoughRetries : SyncDemo public override void Execute(CancellationToken cancellationToken, IProgress progress) { - if (progress == null) throw new ArgumentNullException(nameof(progress)); + ArgumentNullException.ThrowIfNull(progress); - // Let's call a web api service to make repeated requests to a server. + // 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; eventualFailures = 0; + totalRequests = 0; progress.Report(ProgressWithMessage(nameof(Demo03_WaitAndRetryNTimes_WithEnoughRetries))); progress.Report(ProgressWithMessage("======")); progress.Report(ProgressWithMessage(string.Empty)); - // Let's call a web api service to make repeated requests to a server. + // 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. - // Define our policy: - var policy = Policy.Handle().WaitAndRetry( - 20, // Retry up to 20 times! - should be enough that we eventually succeed. - attempt => TimeSpan.FromMilliseconds(200), // Wait 200ms between each try. - (exception, calculatedWaitDuration) => // Capture some info for logging! + // Define our strategy: + var strategy = new ResiliencePipelineBuilder().AddRetry(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + MaxRetryAttempts = 20, // Retry up to 20 times - this should be enough that we eventually succeed. + Delay = TimeSpan.FromMilliseconds(200), // Wait 200ms between each try + OnRetry = args => { - // This is your new exception handler! - // Tell the user what they've won! - progress.Report(ProgressWithMessage("Policy logging: " + exception.Message, Color.Yellow)); + // 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; + } + }).Build(); - using (var client = new WebClient()) + + var client = new HttpClient(); + var internalCancel = false; + + // Do the following until a key is pressed + while (!(internalCancel || cancellationToken.IsCancellationRequested)) { - totalRequests = 0; - var internalCancel = false; + totalRequests++; - // Do the following until a key is pressed - while (!internalCancel && !cancellationToken.IsCancellationRequested) + try { - totalRequests++; - - try + // Retry the following call according to the strategy - 20 times. + // The cancellationToken passed in to Execute() enables the strategy to cancel retries, when the token is signalled. + strategy.Execute(ct => { - // Retry the following call according to the policy - 20 times. - policy.Execute( - ct => // The Execute() overload takes a CancellationToken, but it happens the executed code does not honour it. - { - // This code is executed within the Policy - - // Make a request and get a response - var response = - client.DownloadString(Configuration.WEB_API_ROOT + "/api/values/" + totalRequests); - - // Display the response message on the console - progress.Report(ProgressWithMessage("Response : " + response, Color.Green)); - eventualSuccesses++; - } - , cancellationToken // The cancellationToken passed in to Execute() enables the policy instance to cancel retries, when the token is signalled. - ); - } - catch (Exception e) - { - progress.Report(ProgressWithMessage( - "Request " + totalRequests + " eventually failed with: " + e.Message, Color.Red)); - eventualFailures++; - } - - // Wait half second before the next request. - Thread.Sleep(500); - - internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; + // This code is executed within the strategy + + // 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); + + // Display the response message on the console + using var stream = response.Content.ReadAsStream(ct); + using var streamReader = new StreamReader(stream); + progress.Report(ProgressWithMessage($"Response : {streamReader.ReadToEnd()}", Color.Green)); + eventualSuccesses++; + }, cancellationToken); } + catch (Exception e) + { + progress.Report(ProgressWithMessage($"Request {totalRequests} eventually failed with: {e.Message}", Color.Red)); + eventualFailures++; + } + + Thread.Sleep(500); + internalCancel = TerminateDemosByKeyPress && Console.KeyAvailable; } } - public override Statistic[] LatestStatistics => new[] + public override Statistic[] LatestStatistics => new Statistic[] { - 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 which eventually failed", eventualFailures, Color.Red), + 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 which eventually failed", eventualFailures, Color.Red), }; } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 0b04807..20877d0 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,18 @@ Provides sample implementations using the [Polly library](https://www.github.com/App-vNext/Polly). The intent of this project is to help newcomers kick-start the use of [Polly](https://www.github.com/App-vNext/Polly) within their own projects. The samples demonstrate the policies in action, against faulting endpoints +> [!NOTE] +> **Sample code migration to V8 is in progress!** +> +> As **Polly v8** has reached **feature-complete** state it's time to update the `Polly-Samples` as well. +> The migration process happens gradually so, we are asking for your patience while we get everything updated. + ## About the numbered demos ### Background + The demos run against an example 'faulting server' (also within the solution as `PollyTestWebApi`). To simulate failure, the dummy server rejects more than 3 calls from the same IP in any five-second period (bulkhead demos excepted). -+ Be sure to read the `` at the top of each demo: this explains the intent of that demo, and what resilience it adds to its handling of the calls to the 'faulting server'. ++ Be sure to read the `` at the top of each demo: this explains the intent of that demo, and what resilience it adds to its handling of the calls to the 'faulting server'. + Sometimes the `` also highlights what this demo _doesn't_ achieve - often picked up in the following demo. Explore the demos in sequence, for best understanding. + All demos exist in both sync and async forms. @@ -17,7 +23,7 @@ Provides sample implementations using the [Polly library](https://www.github.com + Demo 00 shows behaviour calling the faulting server every half-second, with _no_ Polly policies protecting the call. + Demos 01-04 show various flavors of Polly retry. -+ Demos 06-07 show retry combined with Circuit-Breaker. ++ Demos 06-07 show retry combined with Circuit-Breaker. + Demo 07 shows the Polly v5.0 `PolicyWrap` for combining policies. + Demo 08 adds Polly v5.0 `Fallback`, making the call protected (in a PolicyWrap) by a Fallback, Retry, Circuitbreaker. + Demo 09 shows the Polly v5.0 `Timeout` policy for an overall call timeout, in combination with `Fallback` and `WaitAndRetry`. @@ -36,10 +42,10 @@ The bulkhead isolation demos place calls against two different endpoints on a do In both bulkhead demos, the upstream system makes a random mixture of calls to the **good** and **faulting** endpoints. -+ In Demo 00 there is **no isolation**: calls to both the **good** and **faulting** endpoints share resources. ++ In Demo 00 there is **no isolation**: calls to both the **good** and **faulting** endpoints share resources. + Sooner or later, the **faulting stream of calls saturates** all resource in the caller, starving the calls to the **good** endpoint of resource too. + Watch how the the calls to the **good** endpoint eventually start backing up (watch the 'pending' or 'faulting' counts climb), because the faulting stream of calls is starving the whole system of resource. -+ In Demo 01, the calls to **faulting** and **good** endpoints are **separated by bulkhead isolation**. ++ In Demo 01, the calls to **faulting** and **good** endpoints are **separated by bulkhead isolation**. + The faulting stream of calls still backs up and fails. + But **the calls to the good endpoint are unaffected - they consistently succeed**, because they are isolated in a separate bulkhead. @@ -71,13 +77,13 @@ Then ... dotnet run --project PollyTestClientConsole/PollyTestClientConsole.csproj ``` -+ To run a demo, uncomment the demo you wish to run in `PollyTestClientConsole\program.cs`. Then start `PollyTestClientConsole`. -+ Many Polly policies are about handling exceptions. If running the demos in debug mode out of Visual Studio and flow is interrupted by Visual Studio breaking on exceptions, uncheck the box "Break when this exception type is user-unhandled" in the dialog shown when Visual Studio breaks on an exception. Or simply run without debugging. ++ To run a demo, uncomment the demo you wish to run in `PollyTestClientConsole\program.cs`. Then start `PollyTestClientConsole`. ++ Many Polly policies are about handling exceptions. If running the demos in debug mode out of Visual Studio and flow is interrupted by Visual Studio breaking on exceptions, uncheck the box "Break when this exception type is user-unhandled" in the dialog shown when Visual Studio breaks on an exception. Or simply run without debugging. ## Want further information? + Any questions about the operation of the demos, ask on this repo; any questions about Polly, ask at [Polly](https://www.github.com/App-vNext/Polly). -+ For full Polly syntax, see [Polly](https://www.github.com/App-vNext/Polly). ++ For full Polly syntax, see [Polly](https://www.github.com/App-vNext/Polly). + For deeper discussions of transient fault-handling and further Polly patterns, see the [Polly documentation](https://www.pollydocs.org/) ## Slide decks