diff --git a/README.md b/README.md index dbc9fd54cb..03d6395351 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,9 @@ > Major performance improvements are on the way! Please see our [blog post](https://www.thepollyproject.org/2023/03/03/we-want-your-feedback-introducing-polly-v8/) to learn more and provide feedback in the [related GitHub issue](https://github.com/App-vNext/Polly/issues/1048). > > :rotating_light::rotating_light: **Polly v8 feature-complete!** :rotating_light::rotating_light: -> - Polly v8 Beta 1 is now available on [NuGet.org](https://www.nuget.org/packages/Polly/8.0.0-beta.1) +> - Polly v8 Beta 1 is now available on [NuGet.org](https://www.nuget.org/packages/Polly/8.0.0-beta.1). > - The Beta 1 version is considered feature-complete and the public API surface is stable. -> - The v8 docs are not yet finished, but you can take a look at sample code in these locations: -> - Within the repo's new [Samples folder](https://github.com/App-vNext/Polly/tree/main/samples) -> - By reading `Polly.Core`'s [README](https://github.com/App-vNext/Polly/blob/main/src/Polly.Core/README.md) +> - Explore the [v8 documentation](README_V8.md). # Polly @@ -56,10 +54,6 @@ dotnet add package Polly For details of supported compilation targets by version, see the [supported targets](https://github.com/App-vNext/Polly/wiki/Supported-targets) grid. -### Using Polly with HttpClient factory from ASP.NET Core 2.1 - -For using Polly with HttpClient factory from ASP.NET Core 2.1, see our [detailed wiki page](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory), then come back here or [explore the wiki](https://github.com/App-vNext/Polly/wiki) to learn more about the operation of each policy. - ### Role of the readme and the wiki This ReadMe aims to give a quick overview of all Polly features - including enough to get you started with any policy. For deeper detail on any policy, and many other aspects of Polly, be sure also to check out the [wiki documentation](https://github.com/App-vNext/Polly/wiki). @@ -1065,8 +1059,4 @@ Licensed under the terms of the [New BSD License](http://opensource.org/licenses ## Resources -- [Extensibility](docs/v7/extensibility.md): Learn how you can extend Polly with new policies. -- [Polly-Contrib](docs/polly-contrib.md): Learn how you can contribute to and enhance the Polly ecosystem. -- [Simmy](docs/simmy.md): Understand chaos engineering through the use of Polly. -- [3rd Party Libraries and Contributions](docs/libraries-and-contributions.md): Discover the libraries that Polly relies on and the contributors who help improve it. -- [Blogs, Podcasts, Courses, E-books, etc.](docs/resources.md): Explore additional community resources. +Visit the [documentation](docs/README.md) to explore more Polly-related resources. diff --git a/README_V8.md b/README_V8.md new file mode 100644 index 0000000000..90b2c9d2c7 --- /dev/null +++ b/README_V8.md @@ -0,0 +1,517 @@ +# Polly + +Polly is a .NET resilience and transient-fault-handling library that allows developers to express resilience strategies such as Retry, Circuit Breaker, Hedging, Timeout, Rate Limiter and Fallback in a fluent and thread-safe manner. + +[](https://www.dotnetfoundation.org/) +We are a member of the [.NET Foundation](https://www.dotnetfoundation.org/about)! + +**Keep up to date with new feature announcements, tips & tricks, and other news through [www.thepollyproject.org](https://www.thepollyproject.org)** + +[![Build status](https://github.com/App-vNext/Polly/workflows/build/badge.svg?branch=main&event=push)](https://github.com/App-vNext/Polly/actions?query=workflow%3Abuild+branch%3Amain+event%3Apush) [![Code coverage](https://codecov.io/gh/App-vNext/Polly/branch/main/graph/badge.svg)](https://codecov.io/gh/App-vNext/Polly) + +![Polly logo](https://raw.github.com/App-vNext/Polly/main/Polly-Logo.png) + +> [!NOTE] +> This README aims to give a quick overview of all Polly features - including enough to get you started with any resilience strategy. For deeper detail on any resilience strategy, and many other aspects of Polly, be sure also to check out the [Polly documentation](docs/README.md). + +> [!IMPORTANT] +> This documentation describes the new Polly v8 API. If you are using the v7 API, please refer to the [previous version](https://github.com/App-vNext/Polly/tree/7.2.4) of the documentation. + +## NuGet Packages + +| **Package** | **Latest Version** | +|:--|:--| +| Polly | [![NuGet](https://buildstats.info/nuget/Polly?includePreReleases=true)](https://www.nuget.org/packages/Polly/ "Download Polly from NuGet.org") | +| Polly.Core | [![NuGet](https://buildstats.info/nuget/Polly.Core?includePreReleases=true)](https://www.nuget.org/packages/Polly.Core/ "Download Polly.Core from NuGet.org") | +| Polly.Extensions | [![NuGet](https://buildstats.info/nuget/Polly.Extensions?includePreReleases=true)](https://www.nuget.org/packages/Polly.Extensions/ "Download Polly.Extensions from NuGet.org") | +| Polly.RateLimiting | [![NuGet](https://buildstats.info/nuget/Polly.RateLimiting?includePreReleases=true)](https://www.nuget.org/packages/Polly.RateLimiting/ "Download Polly.RateLimiting from NuGet.org") | +| Polly.Testing | [![NuGet](https://buildstats.info/nuget/Polly.Testing?includePreReleases=true)](https://www.nuget.org/packages/Polly.Testing/ "Download Polly.Testing from NuGet.org") | + +## Quick start + +To use Polly, you must provide a callback and execute it using [**resilience pipeline**](docs/resilience-pipelines.md). A resilience pipeline is a combination of one or more [**resilience strategies**](docs/resilience-strategies.md) such as retry, timeout, and rate limiter. Polly uses **builders** to integrate these strategies into a pipeline. + +To get started, first add the [Polly.Core](https://www.nuget.org/packages/Polly.Core/) package to your project by running the following command: + +```sh +dotnet add package Polly.Core +``` + +You can create a `ResiliencePipeline` using the `ResiliencePipelineBuilder` class as shown below: + + +```cs +// Create a instance of builder that exposes various extensions for adding resilience strategies +var builder = new ResiliencePipelineBuilder(); + +// Add retry using the default options +builder.AddRetry(new RetryStrategyOptions()); + +// Add 10 second timeout +builder.AddTimeout(TimeSpan.FromSeconds(10)); + +// Build the resilience pipeline +ResiliencePipeline pipeline = builder.Build(); + +// Execute the pipeline +await pipeline.ExecuteAsync(async token => +{ + // Your custom logic here +}); +``` + + +### Dependency injection + +If you prefer to define resilience pipelines using [`IServiceCollection`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection), you'll need to install the [Polly.Extensions](https://www.nuget.org/packages/Polly.Extensions/) package: + +```sh +dotnet add package Polly.Extensions +``` + +You can then define your resilience pipeline using the `AddResiliencePipeline(...)` extension method as shown: + + +```cs +var services = new ServiceCollection(); + +// Define a resilience pipeline with the name "my-pipeline" +services.AddResiliencePipeline("my-pipeline", builder => +{ + builder + .AddRetry(new RetryStrategyOptions()) + .AddTimeout(TimeSpan.FromSeconds(10)); +}); + +// Build the service provider +IServiceProvider serviceProvider = services.BuildServiceProvider(); + +// Retrieve ResiliencePipelineProvider that caches and dynamically creates the resilience pipelines +var pipelineProvider = serviceProvider.GetRequiredService>(); + +// Retrieve resilience pipeline using the name it was registered with +ResiliencePipeline pipeline = pipelineProvider.GetPipeline("my-pipeline"); + +// Execute the pipeline +await pipeline.ExecuteAsync(async token => +{ + // Your custom logic here +}); +``` + + +## Resilience strategies + +Polly provides a variety of resilience strategies. Alongside the comprehensive guides for each strategy, the wiki also includes an [overview of the role each strategy plays in resilience engineering](https://github.com/App-vNext/Polly/wiki/Transient-fault-handling-and-proactive-resilience-engineering). + +Polly categorizes resilience strategies into two main groups: + +- **Reactive**: These strategies handle specific exceptions that are thrown, or results that are returned, by the callbacks executed through the strategy. +- **Proactive**: Unlike reactive strategies, proactive strategies do not focus on handling errors by the callbacks might throw or return. They can make pro-active decisions to cancel or reject the execution of callbacks (e.g., using a rate limiter or a timeout resilience strategy). + +| Strategy | Reactive | Premise | AKA | How does the strategy mitigate?| +| ------------- | --- | ------------- |:-------------: |------------- | +|**Retry**
(strategy family)
([quickstart](#retry) ; [deep](https://github.com/App-vNext/Polly/wiki/Retry)) |Yes|Many faults are transient and may self-correct after a short delay.| *Maybe it's just a blip* | Allows configuring automatic retries. | +|**Circuit-breaker**
(strategy family)
([quickstart](#circuit-breaker) ; [deep](https://github.com/App-vNext/Polly/wiki/Circuit-Breaker))|Yes|When a system is seriously struggling, failing fast is better than making users/callers wait.

Protecting a faulting system from overload can help it recover. | *Stop doing it if it hurts*

*Give that system a break* | Breaks the circuit (blocks executions) for a period, when faults exceed some pre-configured threshold. | +|**Timeout**
([quickstart](#timeout) ; [deep](https://github.com/App-vNext/Polly/wiki/Timeout))|No|Beyond a certain wait, a success result is unlikely.| *Don't wait forever* |Guarantees the caller won't have to wait beyond the timeout. | +|**Rate Limiter**
([quickstart](#rate-limiter) ; [deep](https://github.com/App-vNext/Polly/wiki/Rate-Limit))|No|Limiting the rate a system handles requests is another way to control load.

This can apply to the way your system accepts incoming calls, and/or to the way you call downstream services. | *Slow down a bit, will you?* |Constrains executions to not exceed a certain rate. | +|**Fallback**
([quickstart](#fallback) ; [deep](https://github.com/App-vNext/Polly/wiki/Fallback))|Yes|Things will still fail - plan what you will do when that happens.| *Degrade gracefully* |Defines an alternative value to be returned (or action to be executed) on failure. | +|**Hedging**
([quickstart](#hedging) ; [deep](https://github.com/App-vNext/Polly/wiki/TODO))|Yes|Things can be slow sometimes, plan what you will do when that happens.| *Hedge your bets* | Executes parallel actions when things are slow and waits for the fastest one. | + +Visit [resilience strategies](docs/resilience-strategies.md) docs to explore how to configure individual resilience strategies in more detail. + +### Retry + + +```cs +// Add retry using the default options +new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions()); + +// For instant retries with no delay +new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions +{ + Delay = TimeSpan.Zero +}); + +// For advanced control over the retry behavior, including the number of attempts, +// delay between retries, and the types of exceptions to handle. +new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions +{ + ShouldHandle = new PredicateBuilder().Handle(), + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, // Adds a random factor to the delay + MaxRetryAttempts = 4, + Delay = TimeSpan.FromSeconds(3), +}); + +// To use a custom function to generate the delay for retries +new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions +{ + MaxRetryAttempts = 2, + DelayGenerator = args => + { + var delay = args.AttemptNumber switch + { + 0 => TimeSpan.Zero, + 1 => TimeSpan.FromSeconds(1), + _ => TimeSpan.FromSeconds(5) + }; + + // This example uses a synchronous delay generator, + // but the API also supports asynchronous implementations. + return new ValueTask(delay); + } +}); + +// To extract the delay from the result object +new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions +{ + DelayGenerator = args => + { + if (args.Outcome.Result is HttpResponseMessage responseMessage && + TryGetDelay(responseMessage, out TimeSpan delay)) + { + return new ValueTask(delay); + } + + // Returning null means the retry strategy will use its internal delay for this attempt. + return new ValueTask((TimeSpan?)null); + } +}); + +// To get notifications when a retry is performed +new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions +{ + MaxRetryAttempts = 2, + OnRetry = args => + { + Console.WriteLine("OnRetry, Attempt: {0}", args.AttemptNumber); + + // Event handlers can be asynchronous; here, we return an empty ValueTask. + return default; + } +}); + +// To keep retrying indefinitely until successful +new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions +{ + MaxRetryAttempts = int.MaxValue, +}); +``` + + +If all retries fail, a retry strategy rethrows the final exception back to the calling code. For more details visit the [retry strategy documentation](https://github.com/App-vNext/Polly/wiki/Retry). + +### Circuit Breaker + + +```cs +// Add circuit breaker with default options. +new ResiliencePipelineBuilder().AddCircuitBreaker(new CircuitBreakerStrategyOptions()); + +// Add circuit breaker with customized options: +// +// The circuit will break if more than 50% of actions result in handled exceptions, +// within any 10-second sampling duration, and at least 8 actions are processed. +new ResiliencePipelineBuilder().AddCircuitBreaker(new CircuitBreakerStrategyOptions +{ + FailureRatio = 0.5, + SamplingDuration = TimeSpan.FromSeconds(10), + MinimumThroughput = 8, + BreakDuration = TimeSpan.FromSeconds(30), + ShouldHandle = new PredicateBuilder().Handle() +}); + +// Handle specific failed results for HttpResponseMessage: +new ResiliencePipelineBuilder() + .AddCircuitBreaker(new CircuitBreakerStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(response => response.StatusCode == HttpStatusCode.InternalServerError) + }); + +// Monitor the circuit state, useful for health reporting: +var stateProvider = new CircuitBreakerStateProvider(); + +new ResiliencePipelineBuilder() + .AddCircuitBreaker(new() { StateProvider = stateProvider }) + .Build(); + +/* +CircuitState.Closed - Normal operation; actions are executed. +CircuitState.Open - Circuit is open; actions are blocked. +CircuitState.HalfOpen - Recovery state after break duration expires; actions are permitted. +CircuitState.Isolated - Circuit is manually held open; actions are blocked. +*/ + +// Manually control the Circuit Breaker state: +var manualControl = new CircuitBreakerManualControl(); + +new ResiliencePipelineBuilder() + .AddCircuitBreaker(new() { ManualControl = manualControl }) + .Build(); + +// Manually isolate a circuit, e.g., to isolate a downstream service. +await manualControl.IsolateAsync(); + +// Manually close the circuit to allow actions to be executed again. +await manualControl.CloseAsync(); +``` + + +The Circuit Breaker strategy prevents execution by throwing a `BrokenCircuitException` when the circuit is open. For more details, refer to the [Circuit-Breaker documentation on GitHub](https://github.com/App-vNext/Polly/wiki/Advanced-Circuit-Breaker). + +> [!NOTE] +> Be aware that the Circuit Breaker strategy [rethrows all exceptions](https://github.com/App-vNext/Polly/wiki/Circuit-Breaker#exception-handling), including those that are handled. A Circuit Breaker's role is to monitor faults and break the circuit when a certain threshold is reached; it does not manage retries. Combine the Circuit Breaker with a Retry strategy if needed. + +For more insights on the Circuit Breaker pattern, you can visit: + +- [Making the Netflix API More Resilient](https://techblog.netflix.com/2011/12/making-netflix-api-more-resilient.html) +- [Circuit Breaker by Martin Fowler](https://martinfowler.com/bliki/CircuitBreaker.html) +- [Circuit Breaker Pattern by Microsoft](https://msdn.microsoft.com/en-us/library/dn589784.aspx) +- [Original Circuit Breaking Article](https://web.archive.org/web/20160106203951/http://thatextramile.be/blog/2008/05/the-circuit-breaker) + +### Fallback + + +```cs +// Use a fallback/substitute value if an operation fails. +new ResiliencePipelineBuilder() + .AddFallback(new FallbackStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(r => r is null), + FallbackAction = args => Outcome.FromResultAsValueTask(UserAvatar.Blank) + }); + +// Use a dynamically generated value if an operation fails. +new ResiliencePipelineBuilder() + .AddFallback(new FallbackStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(r => r is null), + FallbackAction = args => + { + var avatar = UserAvatar.GetRandomAvatar(); + return Outcome.FromResultAsValueTask(avatar); + } + }); + +// Use a default or dynamically generated value, and execute an additional action if the fallback is triggered. +new ResiliencePipelineBuilder() + .AddFallback(new FallbackStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(r => r is null), + FallbackAction = args => + { + var avatar = UserAvatar.GetRandomAvatar(); + return Outcome.FromResultAsValueTask(UserAvatar.Blank); + }, + OnFallback = args => + { + // Add extra logic to be executed when the fallback is triggered, such as logging. + return default; // returns an empty ValueTask + } + }); +``` + + +For more details, refer to the [Fallback documentation](https://github.com/App-vNext/Polly/wiki/Fallback). + +### Hedging + + +```cs +// Add hedging with default options. +new ResiliencePipelineBuilder() + .AddHedging(new HedgingStrategyOptions()); + +// Add a customized hedging strategy that retries up to 3 times if the execution +// takes longer than 1 second or if it fails due to an exception or returns an HTTP 500 Internal Server Error. +new ResiliencePipelineBuilder() + .AddHedging(new HedgingStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(response => response.StatusCode == HttpStatusCode.InternalServerError), + MaxHedgedAttempts = 3, + Delay = TimeSpan.FromSeconds(1), + ActionGenerator = args => + { + Console.WriteLine("Preparing to execute hedged action."); + + // Return a delegate function to invoke the original action with the action context. + // Optionally, you can also create a completely new action to be executed. + return () => args.Callback(args.ActionContext); + } + }); + +// Subscribe to hedging events. +new ResiliencePipelineBuilder() + .AddHedging(new HedgingStrategyOptions + { + OnHedging = args => + { + Console.WriteLine($"OnHedging: Attempt number {args.AttemptNumber}"); + return default; + } + }); +``` + + +If all hedged attempts fail, the hedging strategy will either re-throw the last exception or return the final failed result to the caller. For more information, refer to the [hedging strategy documentation](docs/hedging.md). + +### Timeout + +The timeout resilience strategy assumes delegates you execute support [co-operative cancellation](https://learn.microsoft.com/dotnet/standard/threading/cancellation-in-managed-threads). You must use `Execute/Async(...)` overloads taking a `CancellationToken`, and the executed delegate must honor that `CancellationToken`. + + +```cs +// To add timeout using the default options +new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions()); + +// To add a timeout with a custom TimeSpan duration +new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(3)); + +// To add a timeout using a custom timeout generator function +new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions + { + TimeoutGenerator = args => + { + // Note: the timeout generator supports asynchronous operations + return new ValueTask(TimeSpan.FromSeconds(123)); + } + }); + +// To add a timeout and listen for timeout events +new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions + { + TimeoutGenerator = args => + { + // Note: the timeout generator supports asynchronous operations + return new ValueTask(TimeSpan.FromSeconds(123)); + }, + OnTimeout = args => + { + Console.WriteLine($"{args.Context.OperationKey}: Execution timed out after {args.Timeout.TotalSeconds} seconds."); + return default; + } + }); +``` + + +Example execution: + + +```cs +var pipeline = new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(3)) + .Build(); + +HttpResponseMessage httpResponse = await pipeline.ExecuteAsync( + async ct => + { + // Execute a delegate that takes a CancellationToken as an input parameter. + return await httpClient.GetAsync(endpoint, ct); + }, + cancellationToken); +``` + + +Timeout strategies throw `TimeoutRejectedException` when a timeout occurs. For more details see [Timeout strategy documentation](https://github.com/App-vNext/Polly/wiki/Timeout). + +### Rate Limiter + + +```cs +// Add rate limiter with default options. +new ResiliencePipelineBuilder() + .AddRateLimiter(new RateLimiterStrategyOptions()); + +// Create a rate limiter to allow a maximum of 100 concurrent executions and a queue of 50. +new ResiliencePipelineBuilder() + .AddConcurrencyLimiter(100, 50); + +// Create a rate limiter that allows 100 executions per minute. +new ResiliencePipelineBuilder() + .AddRateLimiter(new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions + { + PermitLimit = 100, + Window = TimeSpan.FromMinutes(1) + })); + +// Create a custom partitioned rate limiter. +var partitionedLimiter = PartitionedRateLimiter.Create(context => +{ + // Extract the partition key. + string partitionKey = GetPartitionKey(context); + + return RateLimitPartition.GetConcurrencyLimiter( + partitionKey, + key => new ConcurrencyLimiterOptions + { + PermitLimit = 100 + }); +}); + +new ResiliencePipelineBuilder() + .AddRateLimiter(new RateLimiterStrategyOptions + { + // Provide a custom rate limiter delegate. + RateLimiter = args => + { + return partitionedLimiter.AcquireAsync(args.Context, 1, args.Context.CancellationToken); + } + }); +``` + + +Example execution: + + +```cs +var pipeline = new ResiliencePipelineBuilder().AddConcurrencyLimiter(100, 50).Build(); + +try +{ + // Execute an asynchronous text search operation. + var result = await pipeline.ExecuteAsync( + token => TextSearchAsync(query, token), + cancellationToken); +} +catch (RateLimiterRejectedException ex) +{ + // Handle RateLimiterRejectedException, + // that can optionally contain information about when to retry. + if (ex.RetryAfter is TimeSpan retryAfter) + { + Console.WriteLine($"Retry After: {retryAfter}"); + } +} +``` + + +Rate limiter strategy throws `RateLimiterRejectedException` if execution is rejected. For more details see [Rate Limiter strategy documentation](https://github.com/App-vNext/Polly/wiki/Rate-Limit). + +## Next steps + +To learn more about Polly, visit the [documentation](docs/README.md) or check out the [samples](#samples). + +## Samples + +- [Samples](samples/README.md): Samples in this repository that serve as an introduction to Polly. +- [Polly-Samples](https://github.com/App-vNext/Polly-Samples): Contains practical examples for using various implementations of Polly. Please feel free to contribute to the Polly-Samples repository in order to assist others who are either learning Polly for the first time, or are seeking advanced examples and novel approaches provided by our generous community. +- Microsoft's [eShopOnContainers project](https://github.com/dotnet-architecture/eShopOnContainers): Sample project demonstrating a .NET Microservices architecture and using Polly for resilience. + +## License + +Licensed under the terms of the [New BSD License](http://opensource.org/licenses/BSD-3-Clause) diff --git a/docs/README.md b/docs/README.md index a3daacc109..6b04987065 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,10 +2,22 @@ If you're already familiar with the [basic features](../README.md) of Polly, delve deeper into its advanced functionalities here. -## Table of Contents +## Topics -- [Extensibility](v7/extensibility.md): Learn how you can extend Polly with new policies. +- [General](general.md): General information about Polly. +- [Resilience Pipelines](resilience-pipelines.md): Understanding the use of resilience pipelines. +- [Resilience Strategies](resilience-strategies.md): General information about resilience strategies and how to configure them. +- [Resilience Context](resilience-context.md): Describes the resilience context used by resilience pipelines and strategies. +- [Resilience Pipeline Registry](resilience-pipeline-registry.md): Exploring the registry that stores resilience pipelines. +- [Dependency Injection](dependency-injection.md): How Polly integrates with Dependency Injection. +- [Telemetry](telemetry.md): Insights into telemetry generated by resilience pipelines and strategies. +- [Extensibility](v7/extensibility.md): Learn how you can extend Polly with new resilience strategies. - [Polly-Contrib](polly-contrib.md): Learn how to contribute to and extend the Polly ecosystem. - [Simmy](simmy.md): Get to know chaos engineering via Polly's capabilities. - [Third-Party Libraries and Contributions](libraries-and-contributions.md): Find out which libraries Polly depends on and who contributes to its development. - [Additional Resources](resources.md): Browse through blogs, podcasts, courses, e-books, and other community resources. +- [Using Polly with HttpClientFactory](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory): For using Polly with HttpClientFactory in ASP.NET Core 2.1 and later versions. + +## Topics (previous Polly versions) + +- [Extensibility (v7)](v7/extensibility.md): Learn how you can extend Polly with new policies. diff --git a/docs/dependency-injection.md b/docs/dependency-injection.md new file mode 100644 index 0000000000..829ed5afae --- /dev/null +++ b/docs/dependency-injection.md @@ -0,0 +1,3 @@ +# Dependency Injection + +🚧 This documentation is being written as part of the Polly v8 release. diff --git a/docs/extensiblity.md b/docs/extensiblity.md new file mode 100644 index 0000000000..62ac8e1209 --- /dev/null +++ b/docs/extensiblity.md @@ -0,0 +1,3 @@ +# Extensibility + +🚧 This documentation is being written as part of the Polly v8 release. diff --git a/docs/general.md b/docs/general.md new file mode 100644 index 0000000000..6a243973a8 --- /dev/null +++ b/docs/general.md @@ -0,0 +1,71 @@ +# General + +> [!NOTE] +> This is documentation for the upcoming Polly v8 release. + +## Supported targets + +Polly targets .NET Standard 2.0+ ([coverage](https://docs.microsoft.com/dotnet/standard/net-standard#net-implementation-support): .NET Core 2.0+, .NET Core 3.0, .NET 6.0+ and later Mono, Xamarin and UWP targets). The NuGet package also includes direct targets for .NET Framework 4.6.1 and 4.7.2. + +For details of supported compilation targets by version, see the [supported targets](https://github.com/App-vNext/Polly/wiki/Supported-targets) grid. + +## Asynchronous support + +Polly provides native support for asynchronous operations through all its resilience strategies by offering the `ExecuteAsync` methods on the `ResiliencePipeline` class. + +### SynchronizationContext + +By default, asynchronous continuations and retries do not execute on a captured synchronization context. To modify this behavior, you can use the `ResilienceContext` class and set its `ContinueOnCapturedContext` property to `true`. The following example illustrates this: + + +```cs +// Retrieve an instance of ResilienceContext from the pool +// with the ContinueOnCapturedContext property set to true +ResilienceContext context = ResilienceContextPool.Shared.Get(continueOnCapturedContext: true); + +await pipeline.ExecuteAsync( + async context => + { + // Execute your code, honoring the ContinueOnCapturedContext setting + await MyMethodAsync(context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + }, + context); + +// Optionally, return the ResilienceContext instance back to the pool +// to minimize allocations and enhance performance +ResilienceContextPool.Shared.Return(context); +``` + + +### Cancellation support + +Asynchronous pipeline execution in Polly supports cancellation. This is facilitated through the `ExecuteAsync(...)` method overloads that accept a `CancellationToken`, or by initializing the `ResilienceContext` class with the `CancellationToken` property. + +The `CancellationToken` you pass to the `ExecuteAsync(...)` method serves multiple functions: + +- It cancels resilience actions such as retries, wait times between retries, or rate-limiter leases. +- It is passed to any delegate executed by the strategy as a `CancellationToken` parameter, enabling cancellation during the delegate's execution. +- Is consistent with the .NET Base Class Library's (BCL) behavior in `Task.Run(...)`, if the cancellation token is cancelled before execution begins, the user-defined delegate will not execute at all. + + +```cs +// Execute your code with cancellation support +await pipeline.ExecuteAsync( + async token => await MyMethodAsync(token), + cancellationToken); + +// Use ResilienceContext for more advanced scenarios +ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken: cancellationToken); + +await pipeline.ExecuteAsync( + async context => await MyMethodAsync(context.CancellationToken), + context); +``` + + +## Thread safety + +All Polly resilience strategies are fully thread-safe. You can safely re-use strategies at multiple call sites, and execute through strategies concurrently on different threads. + +> [!IMPORTANT] +> While the internal operation of the strategy is thread-safe, this does not automatically make delegates you execute through the strategy thread-safe: if delegates you execute through the strategy are not thread-safe, they remain not thread-safe. diff --git a/docs/resilience-context.md b/docs/resilience-context.md new file mode 100644 index 0000000000..b6b62b2c6f --- /dev/null +++ b/docs/resilience-context.md @@ -0,0 +1,105 @@ +# Resilience Context + +> [!NOTE] +> This is documentation for the upcoming Polly v8 release. + +The `ResilienceContext` class in Polly provides an execution-scoped instance that accompanies each execution through a Polly resilience strategy. This class serves to share context and facilitate information exchange between the pre-execution, mid-execution, and post-execution phases. + +The resilience context exposes several properties: + +- `OperationKey`: A user-defined identifier for the operation. +- `CancellationToken`: The cancellation token linked to the operation. +- `Properties`: An instance of `ResilienceProperties` for attaching custom data to the context. +- `ContinueOnCapturedContext`: Specifies whether the asynchronous execution should continue on the captured context. + +## Usage + +Below is an example demonstrating how to work with `ResilienceContext`: + + +```cs +// Retrieve a context with a cancellation token +ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken); + +// Attach custom data to the context + +context.Properties.Set(MyResilienceKeys.Key1, "my-data"); +context.Properties.Set(MyResilienceKeys.Key2, 123); + +// Utilize the context in a resilience pipeline +ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddRetry(new() + { + OnRetry = static args => + { + // Retrieve custom data from the context, if available + if (args.Context.Properties.TryGetValue(MyResilienceKeys.Key1, out var data)) + { + Console.WriteLine("OnRetry, Custom Data: {0}", data); + } + + return default; + } + }) + .Build(); + +// Execute the resilience pipeline asynchronously +await pipeline.ExecuteAsync( + async context => + { + // Insert your execution logic here + }, + context); + +// Return the context to the pool +ResilienceContextPool.Shared.Return(context); +``` + + +Where `ResilienceKeys` is defined as: + + +```cs +public static class MyResilienceKeys +{ + public static readonly ResiliencePropertyKey Key1 = new("my-key-1"); + + public static readonly ResiliencePropertyKey Key2 = new("my-key-2"); +} +``` + + +## Resilient context pooling + + +The `ResilienceContext` object is resource-intensive to create, and recreating it for each execution would negatively impact performance. To address this issue, Polly provides a `ResilienceContextPool`. This pool allows you to obtain and reuse `ResilienceContext` instances. Once you've finished using a context instance, you can return it to the pool. This action will reset the context to its initial state, making it available for reuse. + + +The `ResilienceContextPool` offers several `Get` methods. These methods not only allow you to retrieve a `ResilienceContext` instance, but also enable you to initialize some of its properties at the time of retrieval. + + +```cs +// Retrieve a context with a cancellation token +ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken); + +try +{ + // Retrieve a context with a specific operation key + context = ResilienceContextPool.Shared.Get("my-operation-key", cancellationToken); + + // Retrieve a context with multiple properties + context = ResilienceContextPool.Shared.Get( + operationKey: "my-operation-key", + continueOnCapturedContext: true, + cancellationToken: cancellationToken); + + // Use the pool here +} +finally +{ + // Returning the context back to the pool is recommended, but not required as it reduces the allocations. + // It is also OK to not return the context in case of exceptions, if you want to avoid try-catch blocks. + ResilienceContextPool.Shared.Return(context); +} +``` + diff --git a/docs/resilience-pipeline-registry.md b/docs/resilience-pipeline-registry.md new file mode 100644 index 0000000000..06e476142f --- /dev/null +++ b/docs/resilience-pipeline-registry.md @@ -0,0 +1,18 @@ +# Resilience Pipeline Registry + +> [!NOTE] +> This is documentation for the upcoming Polly v8 release. + +The `ResiliencePipelineRegistry` is a generic class that provides the following functionalities: + +- Thread-safe retrieval and dynamic creation of both generic and non-generic resilience pipelines. +- Dynamic reloading of resilience pipelines when configurations change. +- Support for registering both generic and non-generic resilience pipeline builders, enabling dynamic pipeline instance creation. +- Automatic resource management, including disposal of resources tied to resilience pipelines. + +> [!NOTE] +> The generic `TKey` parameter specifies the key type used for caching individual resilience pipelines within the registry. In most use-cases, you will be working with `ResiliencePipelineRegistry`. + +## Usage + +🚧 This documentation is being written as part of the Polly v8 release. diff --git a/docs/resilience-pipelines.md b/docs/resilience-pipelines.md new file mode 100644 index 0000000000..d1d075b819 --- /dev/null +++ b/docs/resilience-pipelines.md @@ -0,0 +1,139 @@ +# Resilience pipelines + +> [!NOTE] +> This is documentation for the upcoming Polly v8 release. + +The `ResiliencePipeline` allows executing arbitrary user-provided callbacks. It is a combination of one or more resilience strategies. + +## Usage + +The `ResiliencePipeline` allow executing various synchronous and asynchronous user-provided callbacks as seen in the examples below: + + +```cs +// Creating a new resilience pipeline +ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddConcurrencyLimiter(100) + .Build(); + +// Executing an asynchronous void callback +await pipeline.ExecuteAsync( + async token => await MyMethodAsync(token), + cancellationToken); + +// Executing a synchronous void callback +pipeline.Execute(() => MyMethod()); + +// Executing an asynchronous callback that returns a value +await pipeline.ExecuteAsync( + async token => await httpClient.GetAsync(endpoint, token), + cancellationToken); + +// Executing an asynchronous callback without allocating a lambda +await pipeline.ExecuteAsync( + static async (state, token) => await state.httpClient.GetAsync(state.endpoint, token), + (httpClient, endpoint), // State provided here + cancellationToken); + +// Executing an asynchronous callback and passing custom data + +// 1. Retrieve a context from the shared pool +ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken); + +// 2. Add custom data to the context +context.Properties.Set(new ResiliencePropertyKey("my-custom-data"), "my-custom-data"); + +// 3. Execute the callback +await pipeline.ExecuteAsync(static async context => +{ + // Retrieve custom data from the context + var customData = context.Properties.GetValue( + new ResiliencePropertyKey("my-custom-data"), + "default-value"); + + Console.WriteLine("Custom Data: {0}", customData); + + await MyMethodAsync(context.CancellationToken); +}, +context); + +// 4. Optionally, return the context to the shared pool +ResilienceContextPool.Shared.Return(context); +``` + + +The above samples demonstrate how to use the resilience pipeline within the same scope. Additionally, consider the following: + +- Separate the resilience pipeline's definition from its usage. Inject pipelines into the code that will consume them. This [facilitates various unit-testing scenarios](https://github.com/App-vNext/Polly/wiki/Unit-testing-with-Polly---with-examples). +- If your application uses Polly in multiple locations, define all pipelines at startup using [`ResiliencePipelineRegistry`](/docs/resilience-pipeline-registry.md) or using the `AddResiliencePipeline` extension. This is a common approach in .NET Core applications. For example, you could create your own extension method on `IServiceCollection` to configure pipelines consumed elsewhere in your application. + + +```cs +public static void ConfigureMyPipelines(IServiceCollection services) +{ + services.AddResiliencePipeline("pipeline-A", builder => builder.AddConcurrencyLimiter(100)); + services.AddResiliencePipeline("pipeline-B", builder => builder.AddRetry(new())); + + // Later, resolve the pipeline by name using ResiliencePipelineProvider or ResiliencePipelineRegistry + var pipelineProvider = services.BuildServiceProvider().GetRequiredService>(); + pipelineProvider.GetPipeline("pipeline-A").Execute(() => { }); +} +``` + + +## Empty resilience pipeline + +The empty resilience pipeline is a special construct that lacks any resilience strategies. You can access it through the following ways: + +- `ResiliencePipeline.Empty` +- `ResiliencePipeline.Empty` + +This is particularly useful in test scenarios where implementing resilience strategies could slow down the test execution or over-complicate test setup. + +## Retrieving execution results with `Outcome` + +The `ResiliencePipeline` class provides the `ExecuteOutcomeAsync(...)` method, which is designed to never throw exceptions. Instead, it stores either the result or the exception within an `Outcome` struct. + + +```cs +// Acquire a ResilienceContext from the pool +ResilienceContext context = ResilienceContextPool.Shared.Get(); + +// Execute the pipeline and store the result in an Outcome +Outcome outcome = await pipeline.ExecuteOutcomeAsync( + static async (context, state) => + { + Console.WriteLine("State: {0}", state); + + try + { + await MyMethodAsync(context.CancellationToken); + + // Use static utility methods from Outcome to easily create an Outcome instance + return Outcome.FromResult(true); + } + catch (Exception e) + { + // Create an Outcome instance that holds the exception + return Outcome.FromException(e); + } + }, + context, + "my-state"); + +// Return the acquired ResilienceContext to the pool +ResilienceContextPool.Shared.Return(context); + +// Evaluate the outcome +if (outcome.Exception is not null) +{ + Console.WriteLine("Execution Failed: {0}", outcome.Exception.Message); +} +else +{ + Console.WriteLine("Execution Result: {0}", outcome.Result); +} +``` + + +Use `ExecuteOutcomeAsync(...)` in high-performance scenarios where you wish to avoid re-throwing exceptions. Keep in mind that Polly's resilience strategies also make use of the `Outcome` struct to prevent unnecessary exception throwing. diff --git a/docs/resilience-strategies.md b/docs/resilience-strategies.md new file mode 100644 index 0000000000..40a246c0c6 --- /dev/null +++ b/docs/resilience-strategies.md @@ -0,0 +1,68 @@ +# Resilience Strategies + +> [!NOTE] +> This is documentation for the upcoming Polly v8 release. + +Resilience strategies are essential components of Polly, designed to execute user-defined callbacks while adding an extra layer of resilience. These strategies can't be executed directly; they must be run through a **resilience pipeline**. Polly provides an API to construct resilience pipelines by incorporating one or more resilience strategies through the pipeline builders. + +## Usage + +Extensions for adding resilience strategies to the builders are provided by each strategy. Depending on the type of strategy, these extensions may be available for both `ResiliencePipelineBuilder` and `ResiliencePipelineBuilder` or just one of them. Proactive strategies like timeout or rate limiter are available for both types of builders, while specialized reactive strategies are only available for `ResiliencePipelineBuilder`. Adding multiple resilience strategies is supported. + +Each resilience strategy provides: + +- Extensions for the resilience strategy builders. +- Configuration options (e.g., `RetryStrategyOptions`) to specify the strategy's behavior. + +Here's an simple example: + + +```cs +ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions + { + Timeout = TimeSpan.FromSeconds(5) + }) + .Build(); +``` + + +> [!NOTE] +> The configuration options are automatically validated by Polly and come with sensible defaults. Therefore, you don't have to specify all the properties unless needed. + +## Fault-handling in reactive strategies + +Each reactive strategy exposes the `ShouldHandle` predicate property. This property represents a predicate to determine whether the fault or the result returned after executing the resilience strategy should be managed or not. + +This is demonstrated below: + + +```cs +// Create an instance of options for a retry strategy. In this example, +// we use RetryStrategyOptions. You could also use other options like +// CircuitBreakerStrategyOptions or FallbackStrategyOptions. +var options = new RetryStrategyOptions(); + +// PredicateBuilder can simplify the setup of the ShouldHandle predicate. +options.ShouldHandle = new PredicateBuilder() + .HandleResult(response => !response.IsSuccessStatusCode) + .Handle(); + +// For greater flexibility, you can directly use the ShouldHandle delegate with switch expressions. +options.ShouldHandle = args => args.Outcome switch +{ + // Strategies may offer additional context for result handling. + // For instance, the retry strategy exposes the number of attempts made. + _ when args.AttemptNumber > 3 => PredicateResult.False(), + { Exception: HttpRequestException } => PredicateResult.True(), + { Result: HttpResponseMessage response } when !response.IsSuccessStatusCode => PredicateResult.True(), + _ => PredicateResult.False() +}; +``` + + +Some additional notes from the preceding example: + +- `PredicateBuilder` is a utility API designed to make configuring predicates easier. +- `PredicateResult.True()` is shorthand for `new ValueTask(true)`. +- All `ShouldHandle` predicates are asynchronous and have the type `Func, ValueTask>`. The `Args` serves as a placeholder, and each strategy defines its own arguments. diff --git a/docs/simmy.md b/docs/simmy.md index e95a71d98e..1d3ed0a825 100644 --- a/docs/simmy.md +++ b/docs/simmy.md @@ -3,4 +3,4 @@ [Simmy][simmy] is a major new companion project adding a chaos-engineering and fault-injection dimension to Polly, through the provision of policies to selectively inject faults or latency. Head over to the [Simmy][simmy] repo to find out more. -[simmy]: https://github.com/Polly-Contrib/Simmy \ No newline at end of file +[simmy]: https://github.com/Polly-Contrib/Simmy diff --git a/docs/telemetry.md b/docs/telemetry.md new file mode 100644 index 0000000000..814a30bd3a --- /dev/null +++ b/docs/telemetry.md @@ -0,0 +1,3 @@ +# Telemetry + +🚧 This documentation is being written as part of the Polly v8 release. diff --git a/src/Polly.Core/README.md b/src/Polly.Core/README.md index 7ceeaced0a..7fad113059 100644 --- a/src/Polly.Core/README.md +++ b/src/Polly.Core/README.md @@ -17,23 +17,23 @@ public abstract class ResiliencePipeline public void Execute(Action callback); public TResult Execute(Func callback); - + public Task ExecuteAsync( - Func callback, + Func callback, CancellationToken cancellationToken = default); - + public Task ExecuteAsync( - Func> callback, + Func> callback, CancellationToken cancellationToken = default); - + public ValueTask ExecuteAsync( - Func callback, + Func callback, CancellationToken cancellationToken = default); - + public ValueTask ExecuteAsync( - Func> callback, + Func> callback, CancellationToken cancellationToken = default); - + // Other methods are omitted for simplicity } ``` @@ -60,13 +60,16 @@ The `ResiliencePipeline` class unifies the four different policies that were ava > [!NOTE] > Polly also provides a `ResiliencePipeline` class. This specialized pipeline is useful for scenarios where the consumer is concerned with only a single type of result. -## Resilience Strategies +### Building resilience pipeline -The resilience pipeline may consist of one or more individual resilience strategies. Polly V8 categorizes resilience strategies into the following building blocks: +- Use `ResiliencePipelineBuilder` to construct an instance of `ResiliencePipeline`. +- Use `ResiliencePipelineBuilder` to construct an instance of `ResiliencePipeline`. - `ResilienceStrategy`: Base class for all proactive resilience strategies. - `ResilienceStrategy`: Base class for all reactive resilience strategies. +Polly provides a variety of extension methods to add resilience strategies to each type of builder. + ### Example: Custom Proactive Strategy Here's an example of a proactive strategy that executes a user-provided callback: @@ -106,6 +109,8 @@ The API exposes the following builder classes for creating resilience pipelines: To construct a resilience pipeline, chain various extensions on the `ResiliencePipelineBuilder` and conclude with a `Build` method call. +Explore [resilience pipelines](../../docs/resilience-pipelines.md) page to explore the consumption of resilience pipelines from the user perspective. + ### Creating a non-generic pipeline @@ -184,7 +189,6 @@ Recommended signatures for these delegates are: - `Func, ValueTask>` (Reactive) - `Func>` (Proactive) -### Delegate Arguments These delegates accept either `Args` or `Args` arguments, which encapsulate event information. Note that all these delegates are asynchronous and return a `ValueTask`. diff --git a/src/Snippets/Docs/CircuitBreaker.cs b/src/Snippets/Docs/CircuitBreaker.cs new file mode 100644 index 0000000000..2fb66df26e --- /dev/null +++ b/src/Snippets/Docs/CircuitBreaker.cs @@ -0,0 +1,69 @@ +using System.Net; +using System.Net.Http; +using Polly; +using Polly.CircuitBreaker; +using Snippets.Docs.Utils; + +namespace Snippets.Docs; + +internal static class CircuitBreaker +{ + public static async Task Usage() + { + #region circuit-breaker + + // Add circuit breaker with default options. + new ResiliencePipelineBuilder().AddCircuitBreaker(new CircuitBreakerStrategyOptions()); + + // Add circuit breaker with customized options: + // + // The circuit will break if more than 50% of actions result in handled exceptions, + // within any 10-second sampling duration, and at least 8 actions are processed. + new ResiliencePipelineBuilder().AddCircuitBreaker(new CircuitBreakerStrategyOptions + { + FailureRatio = 0.5, + SamplingDuration = TimeSpan.FromSeconds(10), + MinimumThroughput = 8, + BreakDuration = TimeSpan.FromSeconds(30), + ShouldHandle = new PredicateBuilder().Handle() + }); + + // Handle specific failed results for HttpResponseMessage: + new ResiliencePipelineBuilder() + .AddCircuitBreaker(new CircuitBreakerStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(response => response.StatusCode == HttpStatusCode.InternalServerError) + }); + + // Monitor the circuit state, useful for health reporting: + var stateProvider = new CircuitBreakerStateProvider(); + + new ResiliencePipelineBuilder() + .AddCircuitBreaker(new() { StateProvider = stateProvider }) + .Build(); + + /* + CircuitState.Closed - Normal operation; actions are executed. + CircuitState.Open - Circuit is open; actions are blocked. + CircuitState.HalfOpen - Recovery state after break duration expires; actions are permitted. + CircuitState.Isolated - Circuit is manually held open; actions are blocked. + */ + + // Manually control the Circuit Breaker state: + var manualControl = new CircuitBreakerManualControl(); + + new ResiliencePipelineBuilder() + .AddCircuitBreaker(new() { ManualControl = manualControl }) + .Build(); + + // Manually isolate a circuit, e.g., to isolate a downstream service. + await manualControl.IsolateAsync(); + + // Manually close the circuit to allow actions to be executed again. + await manualControl.CloseAsync(); + + #endregion + } +} diff --git a/src/Snippets/Docs/Fallback.cs b/src/Snippets/Docs/Fallback.cs new file mode 100644 index 0000000000..3fc375f38b --- /dev/null +++ b/src/Snippets/Docs/Fallback.cs @@ -0,0 +1,65 @@ +using Polly; +using Polly.Fallback; +using Snippets.Docs.Utils; + +namespace Snippets.Docs; + +internal static class Fallback +{ + public static void Usage() + { + #region fallback + + // Use a fallback/substitute value if an operation fails. + new ResiliencePipelineBuilder() + .AddFallback(new FallbackStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(r => r is null), + FallbackAction = args => Outcome.FromResultAsValueTask(UserAvatar.Blank) + }); + + // Use a dynamically generated value if an operation fails. + new ResiliencePipelineBuilder() + .AddFallback(new FallbackStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(r => r is null), + FallbackAction = args => + { + var avatar = UserAvatar.GetRandomAvatar(); + return Outcome.FromResultAsValueTask(avatar); + } + }); + + // Use a default or dynamically generated value, and execute an additional action if the fallback is triggered. + new ResiliencePipelineBuilder() + .AddFallback(new FallbackStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(r => r is null), + FallbackAction = args => + { + var avatar = UserAvatar.GetRandomAvatar(); + return Outcome.FromResultAsValueTask(UserAvatar.Blank); + }, + OnFallback = args => + { + // Add extra logic to be executed when the fallback is triggered, such as logging. + return default; // returns an empty ValueTask + } + }); + + #endregion + } + + public class UserAvatar + { + public static readonly UserAvatar Blank = new(); + + public static UserAvatar GetRandomAvatar() => new(); + } +} diff --git a/src/Snippets/Docs/General.cs b/src/Snippets/Docs/General.cs new file mode 100644 index 0000000000..3880cc6ba1 --- /dev/null +++ b/src/Snippets/Docs/General.cs @@ -0,0 +1,57 @@ +using Polly; + +namespace Snippets.Docs; + +internal static class General +{ + public static async Task SynchronizationContext() + { + ResiliencePipeline pipeline = ResiliencePipeline.Empty; + + #region synchronization-context + + // Retrieve an instance of ResilienceContext from the pool + // with the ContinueOnCapturedContext property set to true + ResilienceContext context = ResilienceContextPool.Shared.Get(continueOnCapturedContext: true); + + await pipeline.ExecuteAsync( + async context => + { + // Execute your code, honoring the ContinueOnCapturedContext setting + await MyMethodAsync(context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext); + }, + context); + + // Optionally, return the ResilienceContext instance back to the pool + // to minimize allocations and enhance performance + ResilienceContextPool.Shared.Return(context); + + #endregion + + static async Task MyMethodAsync(CancellationToken cancellationToken) => await Task.Delay(100, cancellationToken); + } + + public static async Task CancellationTokenSample() + { + ResiliencePipeline pipeline = ResiliencePipeline.Empty; + var cancellationToken = CancellationToken.None; + + #region cancellation-token + + // Execute your code with cancellation support + await pipeline.ExecuteAsync( + async token => await MyMethodAsync(token), + cancellationToken); + + // Use ResilienceContext for more advanced scenarios + ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken: cancellationToken); + + await pipeline.ExecuteAsync( + async context => await MyMethodAsync(context.CancellationToken), + context); + + #endregion + + static async Task MyMethodAsync(CancellationToken cancellationToken) => await Task.Delay(100, cancellationToken); + } +} diff --git a/src/Snippets/Docs/Hedging.cs b/src/Snippets/Docs/Hedging.cs new file mode 100644 index 0000000000..09e2723c1d --- /dev/null +++ b/src/Snippets/Docs/Hedging.cs @@ -0,0 +1,52 @@ +using System.Net; +using System.Net.Http; +using Polly; +using Polly.Hedging; +using Snippets.Docs.Utils; + +namespace Snippets.Docs; + +internal static class Hedging +{ + public static void Usage() + { + #region hedging + + // Add hedging with default options. + new ResiliencePipelineBuilder() + .AddHedging(new HedgingStrategyOptions()); + + // Add a customized hedging strategy that retries up to 3 times if the execution + // takes longer than 1 second or if it fails due to an exception or returns an HTTP 500 Internal Server Error. + new ResiliencePipelineBuilder() + .AddHedging(new HedgingStrategyOptions + { + ShouldHandle = new PredicateBuilder() + .Handle() + .HandleResult(response => response.StatusCode == HttpStatusCode.InternalServerError), + MaxHedgedAttempts = 3, + Delay = TimeSpan.FromSeconds(1), + ActionGenerator = args => + { + Console.WriteLine("Preparing to execute hedged action."); + + // Return a delegate function to invoke the original action with the action context. + // Optionally, you can also create a completely new action to be executed. + return () => args.Callback(args.ActionContext); + } + }); + + // Subscribe to hedging events. + new ResiliencePipelineBuilder() + .AddHedging(new HedgingStrategyOptions + { + OnHedging = args => + { + Console.WriteLine($"OnHedging: Attempt number {args.AttemptNumber}"); + return default; + } + }); + + #endregion + } +} diff --git a/src/Snippets/Docs/RateLimiter.cs b/src/Snippets/Docs/RateLimiter.cs new file mode 100644 index 0000000000..c46886007b --- /dev/null +++ b/src/Snippets/Docs/RateLimiter.cs @@ -0,0 +1,88 @@ +using System.Threading.RateLimiting; +using Polly; +using Polly.RateLimiting; + +namespace Snippets.Docs; + +internal static class RateLimiter +{ + public static void Usage() + { + #region rate-limiter + + // Add rate limiter with default options. + new ResiliencePipelineBuilder() + .AddRateLimiter(new RateLimiterStrategyOptions()); + + // Create a rate limiter to allow a maximum of 100 concurrent executions and a queue of 50. + new ResiliencePipelineBuilder() + .AddConcurrencyLimiter(100, 50); + + // Create a rate limiter that allows 100 executions per minute. + new ResiliencePipelineBuilder() + .AddRateLimiter(new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions + { + PermitLimit = 100, + Window = TimeSpan.FromMinutes(1) + })); + + // Create a custom partitioned rate limiter. + var partitionedLimiter = PartitionedRateLimiter.Create(context => + { + // Extract the partition key. + string partitionKey = GetPartitionKey(context); + + return RateLimitPartition.GetConcurrencyLimiter( + partitionKey, + key => new ConcurrencyLimiterOptions + { + PermitLimit = 100 + }); + }); + + new ResiliencePipelineBuilder() + .AddRateLimiter(new RateLimiterStrategyOptions + { + // Provide a custom rate limiter delegate. + RateLimiter = args => + { + return partitionedLimiter.AcquireAsync(args.Context, 1, args.Context.CancellationToken); + } + }); + + #endregion + } + + public static async Task Execution() + { + var cancellationToken = CancellationToken.None; + var query = "dummy"; + + #region rate-limiter-execution + + var pipeline = new ResiliencePipelineBuilder().AddConcurrencyLimiter(100, 50).Build(); + + try + { + // Execute an asynchronous text search operation. + var result = await pipeline.ExecuteAsync( + token => TextSearchAsync(query, token), + cancellationToken); + } + catch (RateLimiterRejectedException ex) + { + // Handle RateLimiterRejectedException, + // that can optionally contain information about when to retry. + if (ex.RetryAfter is TimeSpan retryAfter) + { + Console.WriteLine($"Retry After: {retryAfter}"); + } + } + + #endregion + } + + private static ValueTask TextSearchAsync(string query, CancellationToken token) => new("dummy"); + + private static string GetPartitionKey(Polly.ResilienceContext context) => string.Empty; +} diff --git a/src/Snippets/Docs/Readme.cs b/src/Snippets/Docs/Readme.cs new file mode 100644 index 0000000000..b0b2d1805b --- /dev/null +++ b/src/Snippets/Docs/Readme.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.DependencyInjection; +using Polly; +using Polly.Registry; +using Polly.Retry; + +namespace Snippets.Docs; + +internal static class Readme +{ + public static async Task QuickStart() + { + #region quick-start + + // Create a instance of builder that exposes various extensions for adding resilience strategies + var builder = new ResiliencePipelineBuilder(); + + // Add retry using the default options + builder.AddRetry(new RetryStrategyOptions()); + + // Add 10 second timeout + builder.AddTimeout(TimeSpan.FromSeconds(10)); + + // Build the resilience pipeline + ResiliencePipeline pipeline = builder.Build(); + + // Execute the pipeline + await pipeline.ExecuteAsync(async token => + { + // Your custom logic here + }); + + #endregion + } + + public static async Task QuickStartDi() + { + #region quick-start-di + + var services = new ServiceCollection(); + + // Define a resilience pipeline with the name "my-pipeline" + services.AddResiliencePipeline("my-pipeline", builder => + { + builder + .AddRetry(new RetryStrategyOptions()) + .AddTimeout(TimeSpan.FromSeconds(10)); + }); + + // Build the service provider + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Retrieve ResiliencePipelineProvider that caches and dynamically creates the resilience pipelines + var pipelineProvider = serviceProvider.GetRequiredService>(); + + // Retrieve resilience pipeline using the name it was registered with + ResiliencePipeline pipeline = pipelineProvider.GetPipeline("my-pipeline"); + + // Execute the pipeline + await pipeline.ExecuteAsync(async token => + { + // Your custom logic here + }); + + #endregion + } +} diff --git a/src/Snippets/Docs/ResilienceContextUsage.cs b/src/Snippets/Docs/ResilienceContextUsage.cs new file mode 100644 index 0000000000..f89130a088 --- /dev/null +++ b/src/Snippets/Docs/ResilienceContextUsage.cs @@ -0,0 +1,94 @@ +using Polly; + +namespace Snippets.Docs; + +internal static class ResilienceContextUsage +{ + #region resilience-keys + + public static class MyResilienceKeys + { + public static readonly ResiliencePropertyKey Key1 = new("my-key-1"); + + public static readonly ResiliencePropertyKey Key2 = new("my-key-2"); + } + + #endregion + + public static async Task Usage() + { + var cancellationToken = CancellationToken.None; + + #region resilience-context + + // Retrieve a context with a cancellation token + ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken); + + // Attach custom data to the context + + context.Properties.Set(MyResilienceKeys.Key1, "my-data"); + context.Properties.Set(MyResilienceKeys.Key2, 123); + + // Utilize the context in a resilience pipeline + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddRetry(new() + { + OnRetry = static args => + { + // Retrieve custom data from the context, if available + if (args.Context.Properties.TryGetValue(MyResilienceKeys.Key1, out var data)) + { + Console.WriteLine("OnRetry, Custom Data: {0}", data); + } + + return default; + } + }) + .Build(); + + // Execute the resilience pipeline asynchronously + await pipeline.ExecuteAsync( + async context => + { + // Insert your execution logic here + }, + context); + + // Return the context to the pool + ResilienceContextPool.Shared.Return(context); + + #endregion + } + + public static async Task Pooling() + { + var cancellationToken = CancellationToken.None; + + #region resilience-context-pool + + // Retrieve a context with a cancellation token + ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken); + + try + { + // Retrieve a context with a specific operation key + context = ResilienceContextPool.Shared.Get("my-operation-key", cancellationToken); + + // Retrieve a context with multiple properties + context = ResilienceContextPool.Shared.Get( + operationKey: "my-operation-key", + continueOnCapturedContext: true, + cancellationToken: cancellationToken); + + // Use the pool here + } + finally + { + // Returning the context back to the pool is recommended, but not required as it reduces the allocations. + // It is also OK to not return the context in case of exceptions, if you want to avoid try-catch blocks. + ResilienceContextPool.Shared.Return(context); + } + + #endregion + } +} diff --git a/src/Snippets/Docs/ResiliencePipelines.cs b/src/Snippets/Docs/ResiliencePipelines.cs new file mode 100644 index 0000000000..b281ca5a00 --- /dev/null +++ b/src/Snippets/Docs/ResiliencePipelines.cs @@ -0,0 +1,140 @@ +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Polly; +using Polly.Registry; + +namespace Snippets.Docs; + +#pragma warning disable CA1031 // Do not catch general exception types + +internal static class ResiliencePipelines +{ + public static async Task Usage() + { + var cancellationToken = CancellationToken.None; + var httpClient = new HttpClient(); + var endpoint = new Uri("https://endpoint"); + + #region resilience-pipeline-usage + + // Creating a new resilience pipeline + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddConcurrencyLimiter(100) + .Build(); + + // Executing an asynchronous void callback + await pipeline.ExecuteAsync( + async token => await MyMethodAsync(token), + cancellationToken); + + // Executing a synchronous void callback + pipeline.Execute(() => MyMethod()); + + // Executing an asynchronous callback that returns a value + await pipeline.ExecuteAsync( + async token => await httpClient.GetAsync(endpoint, token), + cancellationToken); + + // Executing an asynchronous callback without allocating a lambda + await pipeline.ExecuteAsync( + static async (state, token) => await state.httpClient.GetAsync(state.endpoint, token), + (httpClient, endpoint), // State provided here + cancellationToken); + + // Executing an asynchronous callback and passing custom data + + // 1. Retrieve a context from the shared pool + ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken); + + // 2. Add custom data to the context + context.Properties.Set(new ResiliencePropertyKey("my-custom-data"), "my-custom-data"); + + // 3. Execute the callback + await pipeline.ExecuteAsync(static async context => + { + // Retrieve custom data from the context + var customData = context.Properties.GetValue( + new ResiliencePropertyKey("my-custom-data"), + "default-value"); + + Console.WriteLine("Custom Data: {0}", customData); + + await MyMethodAsync(context.CancellationToken); + }, + context); + + // 4. Optionally, return the context to the shared pool + ResilienceContextPool.Shared.Return(context); + + #endregion + } + + #region resilience-pipeline-di-usage + + public static void ConfigureMyPipelines(IServiceCollection services) + { + services.AddResiliencePipeline("pipeline-A", builder => builder.AddConcurrencyLimiter(100)); + services.AddResiliencePipeline("pipeline-B", builder => builder.AddRetry(new())); + + // Later, resolve the pipeline by name using ResiliencePipelineProvider or ResiliencePipelineRegistry + var pipelineProvider = services.BuildServiceProvider().GetRequiredService>(); + pipelineProvider.GetPipeline("pipeline-A").Execute(() => { }); + } + + #endregion + + public static async Task ExecuteOutcomeAsync() + { + var pipeline = Polly.ResiliencePipeline.Empty; + + #region resilience-pipeline-outcome + + // Acquire a ResilienceContext from the pool + ResilienceContext context = ResilienceContextPool.Shared.Get(); + + // Execute the pipeline and store the result in an Outcome + Outcome outcome = await pipeline.ExecuteOutcomeAsync( + static async (context, state) => + { + Console.WriteLine("State: {0}", state); + + try + { + await MyMethodAsync(context.CancellationToken); + + // Use static utility methods from Outcome to easily create an Outcome instance + return Outcome.FromResult(true); + } + catch (Exception e) + { + // Create an Outcome instance that holds the exception + return Outcome.FromException(e); + } + }, + context, + "my-state"); + + // Return the acquired ResilienceContext to the pool + ResilienceContextPool.Shared.Return(context); + + // Evaluate the outcome + if (outcome.Exception is not null) + { + Console.WriteLine("Execution Failed: {0}", outcome.Exception.Message); + } + else + { + Console.WriteLine("Execution Result: {0}", outcome.Result); + } + + #endregion + } + + private static Task MyMethodAsync(CancellationToken cancellationToken) => Task.Delay(1000, cancellationToken); + + private static void MyMethod() + { + // Do nothing + } +} + diff --git a/src/Snippets/Docs/ResilienceStrategies.cs b/src/Snippets/Docs/ResilienceStrategies.cs new file mode 100644 index 0000000000..02fd87080d --- /dev/null +++ b/src/Snippets/Docs/ResilienceStrategies.cs @@ -0,0 +1,51 @@ +using System.Net.Http; +using Polly; +using Polly.Retry; +using Polly.Timeout; + +namespace Snippets.Docs; + +internal static class ResilienceStrategies +{ + public static async Task Usage() + { + #region resilience-strategy-sample + + ResiliencePipeline pipeline = new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions + { + Timeout = TimeSpan.FromSeconds(5) + }) + .Build(); + + #endregion + } + + public static void ShouldHandle() + { + #region should-handle + + // Create an instance of options for a retry strategy. In this example, + // we use RetryStrategyOptions. You could also use other options like + // CircuitBreakerStrategyOptions or FallbackStrategyOptions. + var options = new RetryStrategyOptions(); + + // PredicateBuilder can simplify the setup of the ShouldHandle predicate. + options.ShouldHandle = new PredicateBuilder() + .HandleResult(response => !response.IsSuccessStatusCode) + .Handle(); + + // For greater flexibility, you can directly use the ShouldHandle delegate with switch expressions. + options.ShouldHandle = args => args.Outcome switch + { + // Strategies may offer additional context for result handling. + // For instance, the retry strategy exposes the number of attempts made. + _ when args.AttemptNumber > 3 => PredicateResult.False(), + { Exception: HttpRequestException } => PredicateResult.True(), + { Result: HttpResponseMessage response } when !response.IsSuccessStatusCode => PredicateResult.True(), + _ => PredicateResult.False() + }; + + #endregion + } +} diff --git a/src/Snippets/Docs/Retry.cs b/src/Snippets/Docs/Retry.cs new file mode 100644 index 0000000000..9d1fca8ac0 --- /dev/null +++ b/src/Snippets/Docs/Retry.cs @@ -0,0 +1,105 @@ +using System.Globalization; +using System.Net.Http; +using Polly; +using Polly.Retry; +using Snippets.Docs.Utils; + +namespace Snippets.Docs; + +internal static class Retry +{ + public static void Usage() + { + #region retry + + // Add retry using the default options + new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions()); + + // For instant retries with no delay + new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions + { + Delay = TimeSpan.Zero + }); + + // For advanced control over the retry behavior, including the number of attempts, + // delay between retries, and the types of exceptions to handle. + new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(), + BackoffType = DelayBackoffType.Exponential, + UseJitter = true, // Adds a random factor to the delay + MaxRetryAttempts = 4, + Delay = TimeSpan.FromSeconds(3), + }); + + // To use a custom function to generate the delay for retries + new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions + { + MaxRetryAttempts = 2, + DelayGenerator = args => + { + var delay = args.AttemptNumber switch + { + 0 => TimeSpan.Zero, + 1 => TimeSpan.FromSeconds(1), + _ => TimeSpan.FromSeconds(5) + }; + + // This example uses a synchronous delay generator, + // but the API also supports asynchronous implementations. + return new ValueTask(delay); + } + }); + + // To extract the delay from the result object + new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions + { + DelayGenerator = args => + { + if (args.Outcome.Result is HttpResponseMessage responseMessage && + TryGetDelay(responseMessage, out TimeSpan delay)) + { + return new ValueTask(delay); + } + + // Returning null means the retry strategy will use its internal delay for this attempt. + return new ValueTask((TimeSpan?)null); + } + }); + + // To get notifications when a retry is performed + new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions + { + MaxRetryAttempts = 2, + OnRetry = args => + { + Console.WriteLine("OnRetry, Attempt: {0}", args.AttemptNumber); + + // Event handlers can be asynchronous; here, we return an empty ValueTask. + return default; + } + }); + + // To keep retrying indefinitely until successful + new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions + { + MaxRetryAttempts = int.MaxValue, + }); + + #endregion + } + + private static bool TryGetDelay(HttpResponseMessage response, out TimeSpan delay) + { + if (response.Headers.TryGetValues("Retry-After", out var values) && + values.FirstOrDefault() is string retryAfterValue && + int.TryParse(retryAfterValue, CultureInfo.InvariantCulture, out int retryAfterSeconds)) + { + delay = TimeSpan.FromSeconds(retryAfterSeconds); + return true; + } + + delay = TimeSpan.Zero; + return false; + } +} diff --git a/src/Snippets/Docs/Timeout.cs b/src/Snippets/Docs/Timeout.cs new file mode 100644 index 0000000000..90b7270d83 --- /dev/null +++ b/src/Snippets/Docs/Timeout.cs @@ -0,0 +1,71 @@ +using System; +using System.Net.Http; +using Polly; +using Polly.Timeout; + +namespace Snippets.Docs; + +internal static class Timeout +{ + public static async Task Usage() + { + #region timeout + + // To add timeout using the default options + new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions()); + + // To add a timeout with a custom TimeSpan duration + new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(3)); + + // To add a timeout using a custom timeout generator function + new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions + { + TimeoutGenerator = args => + { + // Note: the timeout generator supports asynchronous operations + return new ValueTask(TimeSpan.FromSeconds(123)); + } + }); + + // To add a timeout and listen for timeout events + new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions + { + TimeoutGenerator = args => + { + // Note: the timeout generator supports asynchronous operations + return new ValueTask(TimeSpan.FromSeconds(123)); + }, + OnTimeout = args => + { + Console.WriteLine($"{args.Context.OperationKey}: Execution timed out after {args.Timeout.TotalSeconds} seconds."); + return default; + } + }); + + #endregion + + var cancellationToken = CancellationToken.None; + var httpClient = new HttpClient(); + var endpoint = new Uri("https://dummy"); + + #region timeout-execution + + var pipeline = new ResiliencePipelineBuilder() + .AddTimeout(TimeSpan.FromSeconds(3)) + .Build(); + + HttpResponseMessage httpResponse = await pipeline.ExecuteAsync( + async ct => + { + // Execute a delegate that takes a CancellationToken as an input parameter. + return await httpClient.GetAsync(endpoint, ct); + }, + cancellationToken); + + #endregion + } +} diff --git a/src/Snippets/Docs/Utils/SomeExceptionType.cs b/src/Snippets/Docs/Utils/SomeExceptionType.cs new file mode 100644 index 0000000000..d3a57b7911 --- /dev/null +++ b/src/Snippets/Docs/Utils/SomeExceptionType.cs @@ -0,0 +1,18 @@ +namespace Snippets.Docs.Utils; + +internal class SomeExceptionType : Exception +{ + public SomeExceptionType(string message) + : base(message) + { + } + + public SomeExceptionType(string message, Exception innerException) + : base(message, innerException) + { + } + + public SomeExceptionType() + { + } +} diff --git a/src/Snippets/Snippets.csproj b/src/Snippets/Snippets.csproj index cedda64e52..3ad377471e 100644 --- a/src/Snippets/Snippets.csproj +++ b/src/Snippets/Snippets.csproj @@ -8,7 +8,8 @@ Library false false - $(NoWarn);SA1123;SA1515;CA2000;CA2007;CA1303;IDE0021 + $(NoWarn);SA1123;SA1515;CA2000;CA2007;CA1303;IDE0021;IDE0017;IDE0060;CS1998;CA1064 + Snippets