From 227dbffe4e07380f7effab87463297871687de14 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Wed, 20 Sep 2023 09:52:33 +0200 Subject: [PATCH 1/6] wip --- docs/advanced/extensibility.md | 191 +++++++++++++++++- .../Proactive/ThresholdExceededArguments.cs | 27 +++ .../Proactive/TimeoutStrategyOptions.cs | 29 +++ .../Proactive/TimingResilienceStrategy.cs | 61 ++++++ ...mingResilienceStrategyBuilderExtensions.cs | 37 ++++ samples/Extensibility/Program.cs | 95 +-------- 6 files changed, 353 insertions(+), 87 deletions(-) create mode 100644 samples/Extensibility/Proactive/ThresholdExceededArguments.cs create mode 100644 samples/Extensibility/Proactive/TimeoutStrategyOptions.cs create mode 100644 samples/Extensibility/Proactive/TimingResilienceStrategy.cs create mode 100644 samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs diff --git a/docs/advanced/extensibility.md b/docs/advanced/extensibility.md index 62ac8e12090..27c692adcd3 100644 --- a/docs/advanced/extensibility.md +++ b/docs/advanced/extensibility.md @@ -1,3 +1,192 @@ # Extensibility -🚧 This documentation is being written as part of the Polly v8 release. +This article explains how to extend Polly with new [resilience strategies](../strategies/index.md). Polly recognizes two families of resilience strategies: + +- **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 proactive decisions to cancel or reject the execution of callbacks (e.g., using a rate limiter or a timeout resilience strategy). + +This article will guide you through the process of creating a new demonstrative resilience strategy for each family type. + +## Basics of extensibility + +Irregardless of whether the strategy is reactive or proactive, every new resilience strategy should expose the following components: + +- The options that describe the configuration of the strategy. These should derive from `ResilienceStrategyOptions`. +- Extensions for `ResiliencePipelineBuilder` or for `ResiliencePipelineBuilder`. +- Custom arguments types used by delegates that hold the information about particular event. + +## Proactive strategy + +This section will guide you through the creation of **Timing Resilience Strategy** that track execution times of callbacks and reports when the execution time took longer that expected. This is a good example of proactive strategy as we do not really care about individual results produced by callbacks. This way, this strategy can used across all types of results. + +### Implementation of proactive strategy + +Proactive resilience strategies derive from [`ResilienceStrategy`](xref:Polly:ResilienceStrategy) base class. In case of this particular strategy, the implementation can look like: + + +```cs +// The strategies should be internal and exposed as part of the library's public API. +// The configuration of strategy should be done via extension methods and options. +internal sealed class TimingResilienceStrategy : ResilienceStrategy +{ + private readonly TimeSpan _threshold; + + private readonly Func? _thresholdExceeded; + + private readonly ResilienceStrategyTelemetry _telemetry; + + public TimingResilienceStrategy( + TimeSpan threshold, + Func? thresholdExceeded, + ResilienceStrategyTelemetry telemetry) + { + _threshold = threshold; + _telemetry = telemetry; + _thresholdExceeded = thresholdExceeded; + } + + protected override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + var stopwatch = Stopwatch.StartNew(); + + // Execute the provided callback and respect the value of ContinueOnCapturedContext property. + Outcome outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + + if (stopwatch.Elapsed > _threshold) + { + // Create arguments that encapsulate the information about the event. + var args = new ThresholdExceededArguments(context, _threshold, stopwatch.Elapsed); + + // Since we detected that this execution took longer than the threshold, we will report this as an resilience event. + _telemetry.Report( + new ResilienceEvent(ResilienceEventSeverity.Warning, "ExecutionThresholdExceeded"), // Pass the event severity and the event name + context, // Forward the context + args); // Forward the arguments so any listeners can recognize this particular event + + if (_thresholdExceeded is not null) + { + await _thresholdExceeded(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + } + + // Just return the outcome + return outcome; + } +} +``` + + +Review the code and comments to learn about the implementation. Notice the `ThresholdExceededArguments` struct: + + +```cs +// Arguments-based structs encapsulate information about particular event that occurred inside resilience strategy. +// They cna expose any properties that are relevant to the event. +// For this event the actual duration of execution and the threshold that was exceeded are relevant. +public readonly struct ThresholdExceededArguments +{ + public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, TimeSpan duration) + { + Context = context; + Threshold = threshold; + Duration = duration; + } + + public TimeSpan Threshold { get; } + + public TimeSpan Duration { get; } + + // By convention, all arguments should expose the "Context" property. + public ResilienceContext Context { get; } +} +``` + + +The arguments should always end with `Arguments` suffix and should contain the `Context` property. Using arguments improves the extensibility and maintainability of the API as adding new members to it is a non-breaking change. + +In this case the `ThresholdExceededArguments` holds information about actual execution time and threshold so any listener can react to this event or provide a custom callback to be executed when this situation occurs. + +### Proactive options + +In previous section we implemented the `TimingResilienceStrategy`. Now, we need to integrate it into Polly and it's public API. + +First let's define public `TimeoutStrategyOptions` that will be used to configure our strategy: + + +```cs +public class TimeoutStrategyOptions : ResilienceStrategyOptions +{ + public TimeoutStrategyOptions() + { + // It's recommended to set the default name for the options so + // the consumer can get additional information in the telemetry. + Name = "Timing"; + } + + // You can use the validation attributes to ensure the options are valid. + // The validation will be performed automatically when building the pipeline. + [Range(typeof(TimeSpan), "00:00:00", "1.00:00:00")] + [Required] + public TimeSpan? Threshold { get; set; } + + // Expose the delegate that will be invoked when the threshold is exceeded. + // The recommendation is that the arguments should have the same name as the delegate but with "Arguments" suffix. + // Notice that the delegate is not required. + public Func? ThresholdExceeded { get; set; } +} +``` + + +The options are our public contract with the consumer. Using them allows easily add new members to it without breaking changes and also perform validation in a standard way. + +### Proactive extensions + +At this point we have: + +- Public `TimeoutStrategyOptions` alongside public arguments used by them. +- Implementation of our proactive strategy - `TimingResilienceStrategy`. + +The final part is to integrate these components by exposing new extensions for the `ResiliencePipelineBuilder` and `ResiliencePipelineBuilder`. Because both builders share the same base class, we can expose a single extension for `ResiliencePipelineBuilderBase` that will work for both derived builders. + + +```cs +public static class TimingResilienceStrategyBuilderExtensions +{ + // The extensions should return the builder for fluent API. + // For proactive strategies we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder" + // by using generic constraints. + public static TBuilder AddTiming(this TBuilder builder, TimeoutStrategyOptions options) + where TBuilder : ResiliencePipelineBuilderBase + { + // The strategy should be added via AddStrategy method that accepts a factory delegate + // and validates the options automatically. + + return builder.AddStrategy( + context => + { + // The "context" contains various properties that can be used by the strategy. + // Here, we just use the "Telemetry" and pass it to the strategy. + // The Threshold and ThresholdExceeded is passed from the options. + var strategy = new TimingResilienceStrategy( + options.Threshold!.Value, + options.ThresholdExceeded, + context.Telemetry); + + return strategy; + }, + options); + } +} +``` + + +### Proactive resources + +To learn more about proactive resilience strategies you can explore the following resources: + +- [Timing Strategy Sample](https://github.com/App-vNext/Polly/tree/main/samples/Extensibility/Proactive): The working sample from this article. +- [Timeout Resilience Startegy] + diff --git a/samples/Extensibility/Proactive/ThresholdExceededArguments.cs b/samples/Extensibility/Proactive/ThresholdExceededArguments.cs new file mode 100644 index 00000000000..5b8543a6a7f --- /dev/null +++ b/samples/Extensibility/Proactive/ThresholdExceededArguments.cs @@ -0,0 +1,27 @@ +using Polly; + +namespace Extensibility.Proactive; + +#region ext-proactive-args + +// Arguments-based structs encapsulate information about particular event that occurred inside resilience strategy. +// They cna expose any properties that are relevant to the event. +// For this event the actual duration of execution and the threshold that was exceeded are relevant. +public readonly struct ThresholdExceededArguments +{ + public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, TimeSpan duration) + { + Context = context; + Threshold = threshold; + Duration = duration; + } + + public TimeSpan Threshold { get; } + + public TimeSpan Duration { get; } + + // By convention, all arguments should expose the "Context" property. + public ResilienceContext Context { get; } +} + +#endregion diff --git a/samples/Extensibility/Proactive/TimeoutStrategyOptions.cs b/samples/Extensibility/Proactive/TimeoutStrategyOptions.cs new file mode 100644 index 00000000000..7f8b8b15545 --- /dev/null +++ b/samples/Extensibility/Proactive/TimeoutStrategyOptions.cs @@ -0,0 +1,29 @@ +using Polly; +using System.ComponentModel.DataAnnotations; + +namespace Extensibility.Proactive; + +#region ext-proactive-options + +public class TimeoutStrategyOptions : ResilienceStrategyOptions +{ + public TimeoutStrategyOptions() + { + // It's recommended to set the default name for the options so + // the consumer can get additional information in the telemetry. + Name = "Timing"; + } + + // You can use the validation attributes to ensure the options are valid. + // The validation will be performed automatically when building the pipeline. + [Range(typeof(TimeSpan), "00:00:00", "1.00:00:00")] + [Required] + public TimeSpan? Threshold { get; set; } + + // Expose the delegate that will be invoked when the threshold is exceeded. + // The recommendation is that the arguments should have the same name as the delegate but with "Arguments" suffix. + // Notice that the delegate is not required. + public Func? ThresholdExceeded { get; set; } +} + +#endregion diff --git a/samples/Extensibility/Proactive/TimingResilienceStrategy.cs b/samples/Extensibility/Proactive/TimingResilienceStrategy.cs new file mode 100644 index 00000000000..850d537e060 --- /dev/null +++ b/samples/Extensibility/Proactive/TimingResilienceStrategy.cs @@ -0,0 +1,61 @@ +using Polly; +using Polly.Telemetry; +using System.Diagnostics; + +namespace Extensibility.Proactive; + +#region ext-proactive-strategy + +// The strategies should be internal and exposed as part of the library's public API. +// The configuration of strategy should be done via extension methods and options. +internal sealed class TimingResilienceStrategy : ResilienceStrategy +{ + private readonly TimeSpan _threshold; + + private readonly Func? _thresholdExceeded; + + private readonly ResilienceStrategyTelemetry _telemetry; + + public TimingResilienceStrategy( + TimeSpan threshold, + Func? thresholdExceeded, + ResilienceStrategyTelemetry telemetry) + { + _threshold = threshold; + _telemetry = telemetry; + _thresholdExceeded = thresholdExceeded; + } + + protected override async ValueTask> ExecuteCore( + Func>> callback, + ResilienceContext context, + TState state) + { + var stopwatch = Stopwatch.StartNew(); + + // Execute the provided callback and respect the value of ContinueOnCapturedContext property. + Outcome outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); + + if (stopwatch.Elapsed > _threshold) + { + // Create arguments that encapsulate the information about the event. + var args = new ThresholdExceededArguments(context, _threshold, stopwatch.Elapsed); + + // Since we detected that this execution took longer than the threshold, we will report this as an resilience event. + _telemetry.Report( + new ResilienceEvent(ResilienceEventSeverity.Warning, "ExecutionThresholdExceeded"), // Pass the event severity and the event name + context, // Forward the context + args); // Forward the arguments so any listeners can recognize this particular event + + if (_thresholdExceeded is not null) + { + await _thresholdExceeded(args).ConfigureAwait(context.ContinueOnCapturedContext); + } + } + + // Just return the outcome + return outcome; + } +} + +#endregion diff --git a/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs b/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs new file mode 100644 index 00000000000..ebe9f828a47 --- /dev/null +++ b/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs @@ -0,0 +1,37 @@ +using Polly; + +namespace Extensibility.Proactive; + +#pragma warning disable IDE0022 // Use expression body for method + +#region ext-proactive-extensions + +public static class TimingResilienceStrategyBuilderExtensions +{ + // The extensions should return the builder for fluent API. + // For proactive strategies we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder" + // by using generic constraints. + public static TBuilder AddTiming(this TBuilder builder, TimeoutStrategyOptions options) + where TBuilder : ResiliencePipelineBuilderBase + { + // The strategy should be added via AddStrategy method that accepts a factory delegate + // and validates the options automatically. + + return builder.AddStrategy( + context => + { + // The "context" contains various properties that can be used by the strategy. + // Here, we just use the "Telemetry" and pass it to the strategy. + // The Threshold and ThresholdExceeded is passed from the options. + var strategy = new TimingResilienceStrategy( + options.Threshold!.Value, + options.ThresholdExceeded, + context.Telemetry); + + return strategy; + }, + options); + } +} + +#endregion diff --git a/samples/Extensibility/Program.cs b/samples/Extensibility/Program.cs index 79d638781c8..012ce302244 100644 --- a/samples/Extensibility/Program.cs +++ b/samples/Extensibility/Program.cs @@ -1,23 +1,25 @@ -using Polly; -using Polly.Telemetry; +using Extensibility.Proactive; +using Polly; +using System.Net.Http.Headers; // ------------------------------------------------------------------------ -// Usage of custom strategy +// Usage of custom proactive strategy // ------------------------------------------------------------------------ var pipeline = new ResiliencePipelineBuilder() // This is custom extension defined in this sample - .AddMyResilienceStrategy(new MySimpleStrategyOptions + .AddTiming(new TimeoutStrategyOptions { - OnCustomEvent = args => + Threshold = TimeSpan.FromSeconds(1), + ThresholdExceeded = args => { - Console.WriteLine("OnCustomEvent"); + Console.WriteLine("Execution threshold exceeded!"); return default; }, }) .Build(); // Execute the pipeline -pipeline.Execute(() => { }); +await pipeline.ExecuteAsync(async token => await Task.Delay(1500, token), CancellationToken.None); // ------------------------------------------------------------------------ // SIMPLE EXTENSIBILITY MODEL (INLINE STRATEGY) @@ -47,85 +49,6 @@ protected override ValueTask> ExecuteCore( } } -// ------------------------------------------------------------------------ -// STANDARD EXTENSIBILITY MODEL -// ------------------------------------------------------------------------ - -// ------------------------------------------------------------------------ -// 1. Create options for your custom strategy -// ------------------------------------------------------------------------ - -// 1.A Define arguments for events that your strategy uses (optional) -public readonly record struct OnCustomEventArguments(ResilienceContext Context); - -// 1.B Define the options. public class MySimpleStrategyOptions : ResilienceStrategyOptions { - // Use the arguments in the delegates. - // The recommendation is to use asynchronous delegates. - public Func? OnCustomEvent { get; set; } -} - -// ------------------------------------------------------------------------ -// 2. Create a custom resilience strategy that derives from ResilienceStrategy -// ------------------------------------------------------------------------ - -// The strategy should be internal and not exposed as part of any public API. -// Instead, expose options and extensions for resilience strategy builder. -// -// For reactive strategies, you can use ReactiveResilienceStrategy as base class. -internal class MyResilienceStrategy : ResilienceStrategy -{ - private readonly ResilienceStrategyTelemetry _telemetry; - private readonly Func? _onCustomEvent; - - public MyResilienceStrategy(ResilienceStrategyTelemetry telemetry, MySimpleStrategyOptions options) - { - _telemetry = telemetry; - _onCustomEvent = options.OnCustomEvent; - } - - protected override async ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - // Here, do something before callback execution - // ... - - // Execute the provided callback - var outcome = await callback(context, state); - - // Here, do something after callback execution - // ... - - // You can then report important telemetry events - _telemetry.Report( - new ResilienceEvent(ResilienceEventSeverity.Information, "MyCustomEvent"), - context, - new OnCustomEventArguments(context)); - - // Call the delegate if provided by the user - if (_onCustomEvent is not null) - { - await _onCustomEvent(new OnCustomEventArguments(context)); - } - - return outcome; - } -} - -// ------------------------------------------------------------------------ -// 3. Expose new extensions for ResiliencePipelineBuilder -// ------------------------------------------------------------------------ -public static class MyResilienceStrategyExtensions -{ - // Add new extension that works for both "ResiliencePipelineBuilder" and "ResiliencePipelineBuilder" - public static TBuilder AddMyResilienceStrategy(this TBuilder builder, MySimpleStrategyOptions options) - where TBuilder : ResiliencePipelineBuilderBase - { - return builder.AddStrategy( - context => new MyResilienceStrategy(context.Telemetry, options), // Provide a factory that creates the strategy - options); // Pass the options, note that the options instance is automatically validated by the builder - } } From 2aa6cd765d8ce29401632cc3679c91b58c1f82ad Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Wed, 20 Sep 2023 12:00:03 +0200 Subject: [PATCH 2/6] wip --- docs/extensibility/index.md | 23 ++++++++++++ .../proactive-strategy.md} | 35 +++++-------------- docs/extensibility/reactive-strategy.md | 1 + docs/index.md | 4 +-- docs/toc.yml | 11 ++++-- 5 files changed, 44 insertions(+), 30 deletions(-) create mode 100644 docs/extensibility/index.md rename docs/{advanced/extensibility.md => extensibility/proactive-strategy.md} (82%) create mode 100644 docs/extensibility/reactive-strategy.md diff --git a/docs/extensibility/index.md b/docs/extensibility/index.md new file mode 100644 index 00000000000..79ecdfcb336 --- /dev/null +++ b/docs/extensibility/index.md @@ -0,0 +1,23 @@ +# Extensibility + +This article explains how to extend Polly with new [resilience strategies](../strategies/index.md). Polly recognizes two families of resilience strategies: + +- **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 proactive decisions to cancel or reject the execution of callbacks (e.g., using a rate limiter or a timeout resilience strategy). + +This article will guide you through the process of creating a new demonstrative resilience strategy for each family type. + +## Basics of extensibility + +Irregardless of whether the strategy is reactive or proactive, every new resilience strategy should expose the following components: + +- The options that describe the configuration of the strategy. These should derive from `ResilienceStrategyOptions`. +- Extensions for `ResiliencePipelineBuilder` or for `ResiliencePipelineBuilder`. +- Custom arguments types used by delegates that hold the information about particular event. + +## How to implement resilience strategy + +Explore the documents bellow to learn more about strategy implementation details: + +- [Proactive strategy](proactive-strategy.md): Describes the process of implementing proactive resilience strategy. +- [Reactive strategy](reactive-strategy.md): Describes the process of implementing reactive resilience strategy. diff --git a/docs/advanced/extensibility.md b/docs/extensibility/proactive-strategy.md similarity index 82% rename from docs/advanced/extensibility.md rename to docs/extensibility/proactive-strategy.md index 27c692adcd3..f842b65b7df 100644 --- a/docs/advanced/extensibility.md +++ b/docs/extensibility/proactive-strategy.md @@ -1,27 +1,10 @@ -# Extensibility - -This article explains how to extend Polly with new [resilience strategies](../strategies/index.md). Polly recognizes two families of resilience strategies: - -- **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 proactive decisions to cancel or reject the execution of callbacks (e.g., using a rate limiter or a timeout resilience strategy). - -This article will guide you through the process of creating a new demonstrative resilience strategy for each family type. - -## Basics of extensibility - -Irregardless of whether the strategy is reactive or proactive, every new resilience strategy should expose the following components: - -- The options that describe the configuration of the strategy. These should derive from `ResilienceStrategyOptions`. -- Extensions for `ResiliencePipelineBuilder` or for `ResiliencePipelineBuilder`. -- Custom arguments types used by delegates that hold the information about particular event. - -## Proactive strategy +# Proactive resilience strategy This section will guide you through the creation of **Timing Resilience Strategy** that track execution times of callbacks and reports when the execution time took longer that expected. This is a good example of proactive strategy as we do not really care about individual results produced by callbacks. This way, this strategy can used across all types of results. -### Implementation of proactive strategy +## Implementation -Proactive resilience strategies derive from [`ResilienceStrategy`](xref:Polly:ResilienceStrategy) base class. In case of this particular strategy, the implementation can look like: +Proactive resilience strategies derive from [`ResilienceStrategy`](xref:Polly.ResilienceStrategy) base class. In case of this particular strategy, the implementation is: ```cs @@ -109,7 +92,7 @@ The arguments should always end with `Arguments` suffix and should contain the ` In this case the `ThresholdExceededArguments` holds information about actual execution time and threshold so any listener can react to this event or provide a custom callback to be executed when this situation occurs. -### Proactive options +## Options In previous section we implemented the `TimingResilienceStrategy`. Now, we need to integrate it into Polly and it's public API. @@ -142,7 +125,7 @@ public class TimeoutStrategyOptions : ResilienceStrategyOptions The options are our public contract with the consumer. Using them allows easily add new members to it without breaking changes and also perform validation in a standard way. -### Proactive extensions +## Extensions At this point we have: @@ -183,10 +166,10 @@ public static class TimingResilienceStrategyBuilderExtensions ``` -### Proactive resources +## Resources To learn more about proactive resilience strategies you can explore the following resources: -- [Timing Strategy Sample](https://github.com/App-vNext/Polly/tree/main/samples/Extensibility/Proactive): The working sample from this article. -- [Timeout Resilience Startegy] - +- [Timing strategy sample](https://github.com/App-vNext/Polly/tree/main/samples/Extensibility/Proactive): The working sample from this article. +- [Timeout resilience strategy]() +- [Rate limiter resilience strategy]() diff --git a/docs/extensibility/reactive-strategy.md b/docs/extensibility/reactive-strategy.md new file mode 100644 index 00000000000..4df5d8cefea --- /dev/null +++ b/docs/extensibility/reactive-strategy.md @@ -0,0 +1 @@ +# Reactive resilience strategy diff --git a/docs/index.md b/docs/index.md index da6e8c2e3cd..5ea87a81e3a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,16 +34,16 @@ Polly has a rich documentation that covers various topics, such as: - [Resilience pipelines](pipelines/index.md): How to combine and reuse strategies in a flexible and modular way. - [Telemetry and monitoring](advanced/telemetry.md): How to access and analyze the data generated by Polly strategies and pipelines. - [Dependency injection](advanced/dependency-injection.md): How to integrate Polly with dependency injection frameworks and containers. -- [Extensibility](advanced/extensibility.md): How to create and use custom strategies and extensions for Polly. - [Performance](advanced/performance.md): Tips on optimizing and getting the best performance from Polly. - [Chaos engineering](advanced/simmy.md): How to use Polly to inject faults and test the resilience of your system. +- [Extensibility](extensibility/index.md): How to create and use custom strategies and extensions for Polly. You can also find many resources and community contributions, such as: - [Samples](https://github.com/App-vNext/Polly/tree/main/samples): Samples in this repository that serve as an introduction to Polly. - [Practical Samples](https://github.com/App-vNext/Polly-Samples): 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. - [Polly-Contrib](community/polly-contrib.md): Community projects and libraries that extend and enhance Polly's functionality and ecosystem. -- [Libraries and contributions](community/libraries-and-contributions): Dependencies and contributors that make Polly possible and awesome. +- [Libraries and contributions](community/libraries-and-contributions.md): Dependencies and contributors that make Polly possible and awesome. - Microsoft's [eShopOnContainers project](https://github.com/dotnet-architecture/eShopOnContainers): Sample project demonstrating a .NET Microservices architecture and using Polly for resilience. You can browse the documentation using the sidebar or visit the [API](api/index.md) section for the reference documentation. diff --git a/docs/toc.yml b/docs/toc.yml index 36f494a5751..f4b182103e0 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -37,8 +37,6 @@ items: - name: Telemetry and monitoring href: advanced/telemetry.md - - name: Extensibility - href: advanced/extensibility.md - name: Dependency injection href: advanced/dependency-injection.md - name: Resilience context @@ -48,6 +46,15 @@ - name: Chaos engineering href: advanced/simmy.md +- name: Extensibility + href: extensibility/index.md + expanded: true + items: + - name: Proactive strategy + href: extensibility/proactive-strategy.md + - name: Reactive strategy + href: extensibility/reactive-strategy.md + - name: Community and resources expanded: true items: From 2320b6d01ecc8b25dbd29b00c6a3d812161e5d67 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Wed, 20 Sep 2023 13:57:57 +0200 Subject: [PATCH 3/6] Cleanup --- docs/extensibility/index.md | 118 ++++++++++- docs/extensibility/proactive-strategy.md | 34 ++- .../Proactive/TimingResilienceStrategy.cs | 2 +- src/Polly.Core/README.md | 194 +----------------- src/Snippets/Core/Snippets.cs | 142 ------------- src/Snippets/Docs/Extensibility.cs | 40 ++++ 6 files changed, 168 insertions(+), 362 deletions(-) delete mode 100644 src/Snippets/Core/Snippets.cs create mode 100644 src/Snippets/Docs/Extensibility.cs diff --git a/docs/extensibility/index.md b/docs/extensibility/index.md index 79ecdfcb336..2a73d5a0938 100644 --- a/docs/extensibility/index.md +++ b/docs/extensibility/index.md @@ -1,23 +1,121 @@ # Extensibility -This article explains how to extend Polly with new [resilience strategies](../strategies/index.md). Polly recognizes two families of resilience strategies: +This article explains how to extend Polly with new [resilience strategies](../strategies/index.md). Polly identifies two types of resilience strategies: - **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 proactive decisions to cancel or reject the execution of callbacks (e.g., using a rate limiter or a timeout resilience strategy). -This article will guide you through the process of creating a new demonstrative resilience strategy for each family type. +This guide will help you create a new illustrative resilience strategy for each type. ## Basics of extensibility -Irregardless of whether the strategy is reactive or proactive, every new resilience strategy should expose the following components: +Regardless of whether the strategy is reactive or proactive, every new resilience strategy should include the following components: -- The options that describe the configuration of the strategy. These should derive from `ResilienceStrategyOptions`. -- Extensions for `ResiliencePipelineBuilder` or for `ResiliencePipelineBuilder`. -- Custom arguments types used by delegates that hold the information about particular event. +- Options detailing the strategy's configuration. These should inherit from [`ResilienceStrategyOptions`](xref:Polly.ResilienceStrategyOptions). +- Extensions for `ResiliencePipelineBuilder` or `ResiliencePipelineBuilder`. +- Custom argument types for delegates that contain information about a specific event. -## How to implement resilience strategy +The strategy options contain properties of following types: -Explore the documents bellow to learn more about strategy implementation details: +- **Primitive types**: Such as `int`, `bool`, `TimeSpan`, etc. +- **Delegates**: For example when strategy need to raise an event, or generate a value. In general, the delegates should by asynchronous. +- **Arguments**: Used by the delegates to pass the information to consumers. -- [Proactive strategy](proactive-strategy.md): Describes the process of implementing proactive resilience strategy. -- [Reactive strategy](reactive-strategy.md): Describes the process of implementing reactive resilience strategy. +## Delegates + +Individual resilience strategies make use of several delegate types: + +- **Predicates**: Vital for determining whether a resilience strategy should handle the given execution result. +- **Events**: Triggered when significant actions or states occur within the resilience strategy. +- **Generators**: Invoked when the resilience strategy needs specific information or values from the caller. + +Recommended signatures for these delegates are: + +### Predicates + +- `Func, ValueTask>` (Reactive) + +### Events + +- `Func, ValueTask>` (Reactive) +- `Func` (Proactive) + +### Generators + +- `Func, ValueTask>` (Reactive) +- `Func>` (Proactive) + +These delegates accept either `Args` or `Args` arguments, which encapsulate event information. Note that all these delegates are asynchronous and return a `ValueTask`. Learn more about [arguments](#arguments) in the sections bellow. + +> [!NOTE] +> When setting up delegates, consider using the `ResilienceContext.ContinueOnCapturedContext` property if your user code interacts with a synchronization context (as in asynchronous UI applications like Windows Forms or WPF). + +### How to use delegates + +Below are some examples illustrating the usage of these delegates: + + +```cs +new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Non-Generic predicate for multiple result types + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: string result } when result == "Failure" => PredicateResult.True(), + { Result: int result } when result == -1 => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); + +new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Generic predicate for a single result type + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: { } result } when result == "Failure" => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); +``` + + +## Arguments + +Arguments are used by individual delegate types to flow information to the consumer. Arguments should always have an `Arguments` suffix and include a `Context` property. Using arguments boosts the extensibility and maintainability of the API, as adding new members becomes a non-breaking change. For proactive strategies, the arguments structure might resemble: + + +```cs +// Arguments-based structs encapsulate information about particular event that occurred inside resilience strategy. +// They cna expose any properties that are relevant to the event. +// For this event the actual duration of execution and the threshold that was exceeded are relevant. +public readonly struct ThresholdExceededArguments +{ + public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, TimeSpan duration) + { + Context = context; + Threshold = threshold; + Duration = duration; + } + + public TimeSpan Threshold { get; } + + public TimeSpan Duration { get; } + + // By convention, all arguments should expose the "Context" property. + public ResilienceContext Context { get; } +} +``` + + +## Implementing a resilience strategy + +To understand the details of implementing a strategy, use the links below: + +- [Proactive strategy](proactive-strategy.md): Explains how to implement a proactive resilience strategy. +- [Reactive strategy](reactive-strategy.md): Explains how to implement a reactive resilience strategy. diff --git a/docs/extensibility/proactive-strategy.md b/docs/extensibility/proactive-strategy.md index f842b65b7df..e78ef03c539 100644 --- a/docs/extensibility/proactive-strategy.md +++ b/docs/extensibility/proactive-strategy.md @@ -1,14 +1,14 @@ # Proactive resilience strategy -This section will guide you through the creation of **Timing Resilience Strategy** that track execution times of callbacks and reports when the execution time took longer that expected. This is a good example of proactive strategy as we do not really care about individual results produced by callbacks. This way, this strategy can used across all types of results. +This section guides you in creating a **Timing resilience strategy** that tracks the execution times of callbacks and reports when the execution time exceeds the expected duration. This is a prime example of a proactive strategy because we aren't concerned with the individual results produced by the callbacks. Hence, this strategy can be used across various result types. ## Implementation -Proactive resilience strategies derive from [`ResilienceStrategy`](xref:Polly.ResilienceStrategy) base class. In case of this particular strategy, the implementation is: +Proactive resilience strategies are derived from the [`ResilienceStrategy`](xref:Polly.ResilienceStrategy) base class. For this strategy, the implementation is: ```cs -// The strategies should be internal and exposed as part of the library's public API. +// The strategies should be internal and not exposed as part of the library's public API. // The configuration of strategy should be done via extension methods and options. internal sealed class TimingResilienceStrategy : ResilienceStrategy { @@ -62,7 +62,7 @@ internal sealed class TimingResilienceStrategy : ResilienceStrategy ``` -Review the code and comments to learn about the implementation. Notice the `ThresholdExceededArguments` struct: +Review the code and comments to understand the implementation. Take note of the `ThresholdExceededArguments` struct: ```cs @@ -88,15 +88,13 @@ public readonly struct ThresholdExceededArguments ``` -The arguments should always end with `Arguments` suffix and should contain the `Context` property. Using arguments improves the extensibility and maintainability of the API as adding new members to it is a non-breaking change. - -In this case the `ThresholdExceededArguments` holds information about actual execution time and threshold so any listener can react to this event or provide a custom callback to be executed when this situation occurs. +Arguments should always have an `Arguments` suffix and include a `Context` property. Using arguments boosts the extensibility and maintainability of the API, as adding new members becomes a non-breaking change. The `ThresholdExceededArguments` provides details about the actual execution time and threshold, allowing listeners to respond to this event or supply a custom callback for such situations. ## Options -In previous section we implemented the `TimingResilienceStrategy`. Now, we need to integrate it into Polly and it's public API. +In the previous section, we implemented the `TimingResilienceStrategy`. Now, it's time to integrate it with Polly and its public API. -First let's define public `TimeoutStrategyOptions` that will be used to configure our strategy: +Let's define the public `TimeoutStrategyOptions` to configure our strategy: ```cs @@ -123,16 +121,16 @@ public class TimeoutStrategyOptions : ResilienceStrategyOptions ``` -The options are our public contract with the consumer. Using them allows easily add new members to it without breaking changes and also perform validation in a standard way. +Options represent our public contract with the consumer. By using them, we can easily add new members without breaking changes and perform validation consistently. ## Extensions -At this point we have: +So far, we have: -- Public `TimeoutStrategyOptions` alongside public arguments used by them. -- Implementation of our proactive strategy - `TimingResilienceStrategy`. +- Public `TimeoutStrategyOptions` and the public arguments associated with them. +- Our proactive strategy implementation - `TimingResilienceStrategy`. -The final part is to integrate these components by exposing new extensions for the `ResiliencePipelineBuilder` and `ResiliencePipelineBuilder`. Because both builders share the same base class, we can expose a single extension for `ResiliencePipelineBuilderBase` that will work for both derived builders. +The last step is to combine these components by introducing new extensions for the `ResiliencePipelineBuilder` and `ResiliencePipelineBuilder`. As both builders share the same base class, we can present a single extension for `ResiliencePipelineBuilderBase` to cater to both. ```cs @@ -168,8 +166,8 @@ public static class TimingResilienceStrategyBuilderExtensions ## Resources -To learn more about proactive resilience strategies you can explore the following resources: +For further understanding of proactive resilience strategies, consider exploring these resources: -- [Timing strategy sample](https://github.com/App-vNext/Polly/tree/main/samples/Extensibility/Proactive): The working sample from this article. -- [Timeout resilience strategy]() -- [Rate limiter resilience strategy]() +- [Timing strategy sample](https://github.com/App-vNext/Polly/tree/main/samples/Extensibility/Proactive): A practical example from this guide. +- [Timeout resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.Core/Timeout): Discover the built-in timeout resilience strategy implementation. +- [Rate limiter resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.RateLimiting): DIscover how rate limiter strategy is implemented. diff --git a/samples/Extensibility/Proactive/TimingResilienceStrategy.cs b/samples/Extensibility/Proactive/TimingResilienceStrategy.cs index 850d537e060..4f5dfb940df 100644 --- a/samples/Extensibility/Proactive/TimingResilienceStrategy.cs +++ b/samples/Extensibility/Proactive/TimingResilienceStrategy.cs @@ -6,7 +6,7 @@ namespace Extensibility.Proactive; #region ext-proactive-strategy -// The strategies should be internal and exposed as part of the library's public API. +// The strategies should be internal and not exposed as part of the library's public API. // The configuration of strategy should be done via extension methods and options. internal sealed class TimingResilienceStrategy : ResilienceStrategy { diff --git a/src/Polly.Core/README.md b/src/Polly.Core/README.md index b9ea2561885..94c865af273 100644 --- a/src/Polly.Core/README.md +++ b/src/Polly.Core/README.md @@ -1,10 +1,6 @@ # Polly V8 API Documentation -The Polly V8 API offers a unified, non-allocating resilience API, detailed in the sections below. - -## Introduction - -At the core of Polly V8 is the [`ResiliencePipeline`](ResiliencePipeline.cs) class, responsible for executing user-provided callbacks. This class handles all scenarios covered in Polly V7, such as: +The Polly V8 API offers a unified, non-allocating resilience API. At the core of Polly V8 is the [`ResiliencePipeline`](ResiliencePipeline.cs) class, responsible for executing user-provided callbacks. This class handles all scenarios covered in Polly V7, such as: - `ISyncPolicy` - `IAsyncPolicy` @@ -60,190 +56,6 @@ 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. -### Building resilience pipeline - -- 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: - - -```cs -internal class MyCustomStrategy : ResilienceStrategy -{ - protected override async ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - // Perform actions before execution - - var outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); - - // Perform actions after execution - - return outcome; - } -} -``` - - -### About Synchronous and Asynchronous Executions - -Polly's core, from version 8, fundamentally focuses on asynchronous executions. However, it also supports synchronous executions, which require minimal effort for authors developing custom resilience strategies. This support is enabled by passing and wrapping the synchronous callback provided by the user into an asynchronous one, which returns a completed `ValueTask` upon completion. This feature allows custom resilience strategies to treat all executions as asynchronous. In cases of synchronous execution, the method simply returns a completed task upon awaiting. - -## Creating a `ResiliencePipeline` - -The API exposes the following builder classes for creating resilience pipelines: - -- [`ResiliencePipelineBuilder`](ResiliencePipelineBuilder.cs): Useful for creating resilience strategies capable of executing any type of callback. Typically, these strategies are focused on exception handling. -- [`ResiliencePipelineBuilder`](ResiliencePipelineBuilder.TResult.cs): Aimed at creating generic resilience strategies that execute callbacks returning a specific result type. -- [`ResiliencePipelineBuilderBase`](ResiliencePipelineBuilderBase.cs): This serves as the base class for both of the builders mentioned above. It can be used as a target for strategy extensions compatible with either of the two. - -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 - - -```cs -ResiliencePipeline pipeline = new ResiliencePipelineBuilder() - .AddRetry(new()) - .AddCircuitBreaker(new()) - .AddTimeout(TimeSpan.FromSeconds(1)) - .Build(); -``` - - -### Creating a generic pipeline - - -```cs -ResiliencePipeline pipeline = new ResiliencePipelineBuilder() - .AddRetry(new()) - .AddCircuitBreaker(new()) - .AddTimeout(TimeSpan.FromSeconds(1)) - .Build(); -``` - - -## Extensibility - -Extending the resilience functionality is straightforward. You can create extensions for `ResiliencePipelineBuilder` by leveraging the `AddStrategy` extension methods. If you aim to design a resilience strategy that is compatible with both generic and non-generic builders, consider using `ResiliencePipelineBuilderBase` as your target class. - -Here's an example: +## Resources - -```cs -public static TBuilder AddMyCustomStrategy(this TBuilder builder, MyCustomStrategyOptions options) - where TBuilder : ResiliencePipelineBuilderBase -{ - return builder.AddStrategy(context => new MyCustomStrategy(), options); -} - -public class MyCustomStrategyOptions : ResilienceStrategyOptions -{ - public MyCustomStrategyOptions() - { - Name = "MyCustomStrategy"; - } -} -``` - - -To gain insights into implementing custom resilience strategies, you can explore the following Polly strategy examples: - -- [**Retry**](Retry/): Demonstrates how to implement a reactive resilience strategy. -- [**Timeout**](Timeout/): Provides guidance on implementing a proactive resilience strategy. -- [**Extensibility Sample**](../../samples/Extensibility/): Offers a practical example of creating a custom resilience strategy. - -## Resilience Strategy Delegates - -Individual resilience strategies make use of several delegate types: - -- **Predicates**: Vital for determining whether a resilience strategy should handle the given execution result. -- **Events**: Triggered when significant actions or states occur within the resilience strategy. -- **Generators**: Invoked when the resilience strategy needs specific information or values from the caller. - -Recommended signatures for these delegates are: - -### Predicates - -- `Func, ValueTask>` (Reactive) - -### Events - -- `Func, ValueTask>` (Reactive) -- `Func` (Proactive) - -### Generators - -- `Func, ValueTask>` (Reactive) -- `Func>` (Proactive) - -These delegates accept either `Args` or `Args` arguments, which encapsulate event information. Note that all these delegates are asynchronous and return a `ValueTask`. - -> [!NOTE] -> When setting up delegates, consider using the `ResilienceContext.ContinueOnCapturedContext` property if your user code interacts with a synchronization context (as in asynchronous UI applications like Windows Forms or WPF). - -For proactive strategies, the `Args` structure might resemble: - - -```cs -public readonly struct OnTimeoutArguments -{ - public OnTimeoutArguments(ResilienceContext context, TimeSpan timeout) - { - Context = context; - Timeout = timeout; - } - - public ResilienceContext Context { get; } // Include the Context property - - public TimeSpan Timeout { get; } // Additional event-related properties -} -``` - - -### Example: Usage of Delegates - -Below are some examples illustrating the usage of these delegates: - - -```cs -new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - // Non-Generic predicate for multiple result types - ShouldHandle = args => args.Outcome switch - { - { Exception: InvalidOperationException } => PredicateResult.True(), - { Result: string result } when result == "Failure" => PredicateResult.True(), - { Result: int result } when result == -1 => PredicateResult.True(), - _ => PredicateResult.False() - }, - }) - .Build(); - -new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - // Generic predicate for a single result type - ShouldHandle = args => args.Outcome switch - { - { Exception: InvalidOperationException } => PredicateResult.True(), - { Result: { } result } when result == "Failure" => PredicateResult.True(), - _ => PredicateResult.False() - }, - }) - .Build(); -``` - +Visit to learn more about Polly. diff --git a/src/Snippets/Core/Snippets.cs b/src/Snippets/Core/Snippets.cs deleted file mode 100644 index 92d6dc0d542..00000000000 --- a/src/Snippets/Core/Snippets.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Polly.Retry; - -namespace Snippets.Core; - -internal static class Snippets -{ - public static void NonGenericPipeline() - { - #region create-non-generic-pipeline - - ResiliencePipeline pipeline = new ResiliencePipelineBuilder() - .AddRetry(new()) - .AddCircuitBreaker(new()) - .AddTimeout(TimeSpan.FromSeconds(1)) - .Build(); - - #endregion - } - - public static void GenericPipeline() - { - #region create-generic-pipeline - - ResiliencePipeline pipeline = new ResiliencePipelineBuilder() - .AddRetry(new()) - .AddCircuitBreaker(new()) - .AddTimeout(TimeSpan.FromSeconds(1)) - .Build(); - - #endregion - } - - #region on-retry-args - - public readonly struct OnRetryArguments - { - public OnRetryArguments(ResilienceContext context, Outcome outcome, int attemptNumber) - { - Context = context; - Outcome = outcome; - AttemptNumber = attemptNumber; - } - - public ResilienceContext Context { get; } // Include the Context property - - public Outcome Outcome { get; } // Includes the outcome associated with the event - - public int AttemptNumber { get; } - } - - #endregion - - #region on-timeout-args - - public readonly struct OnTimeoutArguments - { - public OnTimeoutArguments(ResilienceContext context, TimeSpan timeout) - { - Context = context; - Timeout = timeout; - } - - public ResilienceContext Context { get; } // Include the Context property - - public TimeSpan Timeout { get; } // Additional event-related properties - } - - #endregion - - #region add-my-custom-strategy - - public static TBuilder AddMyCustomStrategy(this TBuilder builder, MyCustomStrategyOptions options) - where TBuilder : ResiliencePipelineBuilderBase - { - return builder.AddStrategy(context => new MyCustomStrategy(), options); - } - - public class MyCustomStrategyOptions : ResilienceStrategyOptions - { - public MyCustomStrategyOptions() - { - Name = "MyCustomStrategy"; - } - } - - #endregion - - #region my-custom-strategy - - internal class MyCustomStrategy : ResilienceStrategy - { - protected override async ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - // Perform actions before execution - - var outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); - - // Perform actions after execution - - return outcome; - } - } - - #endregion - - public static void DelegateUsage() - { - #region delegate-usage - - new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - // Non-Generic predicate for multiple result types - ShouldHandle = args => args.Outcome switch - { - { Exception: InvalidOperationException } => PredicateResult.True(), - { Result: string result } when result == "Failure" => PredicateResult.True(), - { Result: int result } when result == -1 => PredicateResult.True(), - _ => PredicateResult.False() - }, - }) - .Build(); - - new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - // Generic predicate for a single result type - ShouldHandle = args => args.Outcome switch - { - { Exception: InvalidOperationException } => PredicateResult.True(), - { Result: { } result } when result == "Failure" => PredicateResult.True(), - _ => PredicateResult.False() - }, - }) - .Build(); - - #endregion; - } -} diff --git a/src/Snippets/Docs/Extensibility.cs b/src/Snippets/Docs/Extensibility.cs new file mode 100644 index 00000000000..95348bd1f8a --- /dev/null +++ b/src/Snippets/Docs/Extensibility.cs @@ -0,0 +1,40 @@ +using Polly.Retry; + +namespace Snippets.Docs; + +internal static class Extensibility +{ + public static void DelegateUsage() + { + #region delegate-usage + + new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Non-Generic predicate for multiple result types + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: string result } when result == "Failure" => PredicateResult.True(), + { Result: int result } when result == -1 => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); + + new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + // Generic predicate for a single result type + ShouldHandle = args => args.Outcome switch + { + { Exception: InvalidOperationException } => PredicateResult.True(), + { Result: { } result } when result == "Failure" => PredicateResult.True(), + _ => PredicateResult.False() + }, + }) + .Build(); + + #endregion; + } +} From 4f3c832529e7cfb9df5649f4f75b3dc753cb2041 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Wed, 20 Sep 2023 14:06:13 +0200 Subject: [PATCH 4/6] Cleanup --- docs/extensibility/proactive-strategy.md | 6 +++--- .../Proactive/TimingResilienceStrategyBuilderExtensions.cs | 2 +- .../{TimeoutStrategyOptions.cs => TimingStrategyOptions.cs} | 4 ++-- samples/Extensibility/Program.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename samples/Extensibility/Proactive/{TimeoutStrategyOptions.cs => TimingStrategyOptions.cs} (90%) diff --git a/docs/extensibility/proactive-strategy.md b/docs/extensibility/proactive-strategy.md index e78ef03c539..e0256d3b0f3 100644 --- a/docs/extensibility/proactive-strategy.md +++ b/docs/extensibility/proactive-strategy.md @@ -98,9 +98,9 @@ Let's define the public `TimeoutStrategyOptions` to configure our strategy: ```cs -public class TimeoutStrategyOptions : ResilienceStrategyOptions +public class TimingStrategyOptions : ResilienceStrategyOptions { - public TimeoutStrategyOptions() + public TimingStrategyOptions() { // It's recommended to set the default name for the options so // the consumer can get additional information in the telemetry. @@ -139,7 +139,7 @@ public static class TimingResilienceStrategyBuilderExtensions // The extensions should return the builder for fluent API. // For proactive strategies we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder" // by using generic constraints. - public static TBuilder AddTiming(this TBuilder builder, TimeoutStrategyOptions options) + public static TBuilder AddTiming(this TBuilder builder, TimingStrategyOptions options) where TBuilder : ResiliencePipelineBuilderBase { // The strategy should be added via AddStrategy method that accepts a factory delegate diff --git a/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs b/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs index ebe9f828a47..ccd358454dc 100644 --- a/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs +++ b/samples/Extensibility/Proactive/TimingResilienceStrategyBuilderExtensions.cs @@ -11,7 +11,7 @@ public static class TimingResilienceStrategyBuilderExtensions // The extensions should return the builder for fluent API. // For proactive strategies we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder" // by using generic constraints. - public static TBuilder AddTiming(this TBuilder builder, TimeoutStrategyOptions options) + public static TBuilder AddTiming(this TBuilder builder, TimingStrategyOptions options) where TBuilder : ResiliencePipelineBuilderBase { // The strategy should be added via AddStrategy method that accepts a factory delegate diff --git a/samples/Extensibility/Proactive/TimeoutStrategyOptions.cs b/samples/Extensibility/Proactive/TimingStrategyOptions.cs similarity index 90% rename from samples/Extensibility/Proactive/TimeoutStrategyOptions.cs rename to samples/Extensibility/Proactive/TimingStrategyOptions.cs index 7f8b8b15545..6f449372826 100644 --- a/samples/Extensibility/Proactive/TimeoutStrategyOptions.cs +++ b/samples/Extensibility/Proactive/TimingStrategyOptions.cs @@ -5,9 +5,9 @@ namespace Extensibility.Proactive; #region ext-proactive-options -public class TimeoutStrategyOptions : ResilienceStrategyOptions +public class TimingStrategyOptions : ResilienceStrategyOptions { - public TimeoutStrategyOptions() + public TimingStrategyOptions() { // It's recommended to set the default name for the options so // the consumer can get additional information in the telemetry. diff --git a/samples/Extensibility/Program.cs b/samples/Extensibility/Program.cs index 012ce302244..d684bd29aae 100644 --- a/samples/Extensibility/Program.cs +++ b/samples/Extensibility/Program.cs @@ -7,7 +7,7 @@ // ------------------------------------------------------------------------ var pipeline = new ResiliencePipelineBuilder() // This is custom extension defined in this sample - .AddTiming(new TimeoutStrategyOptions + .AddTiming(new TimingStrategyOptions { Threshold = TimeSpan.FromSeconds(1), ThresholdExceeded = args => From 16bc1a50b25c118f082281d6a867966f13220029 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Wed, 20 Sep 2023 14:11:12 +0200 Subject: [PATCH 5/6] Cleanup --- docs/extensibility/proactive-strategy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extensibility/proactive-strategy.md b/docs/extensibility/proactive-strategy.md index e0256d3b0f3..6ac8bf3b3d1 100644 --- a/docs/extensibility/proactive-strategy.md +++ b/docs/extensibility/proactive-strategy.md @@ -94,7 +94,7 @@ Arguments should always have an `Arguments` suffix and include a `Context` prope In the previous section, we implemented the `TimingResilienceStrategy`. Now, it's time to integrate it with Polly and its public API. -Let's define the public `TimeoutStrategyOptions` to configure our strategy: +Let's define the public `TimingResilienceStrategy` to configure our strategy: ```cs @@ -127,7 +127,7 @@ Options represent our public contract with the consumer. By using them, we can e So far, we have: -- Public `TimeoutStrategyOptions` and the public arguments associated with them. +- Public `TimingResilienceStrategy` and the public arguments associated with them. - Our proactive strategy implementation - `TimingResilienceStrategy`. The last step is to combine these components by introducing new extensions for the `ResiliencePipelineBuilder` and `ResiliencePipelineBuilder`. As both builders share the same base class, we can present a single extension for `ResiliencePipelineBuilderBase` to cater to both. From 60f14568daad9ab6abdd7f924d349003832f38f7 Mon Sep 17 00:00:00 2001 From: Martin Tomka Date: Thu, 21 Sep 2023 08:10:03 +0200 Subject: [PATCH 6/6] Cleanup and grammar --- docs/extensibility/index.md | 9 ++- docs/extensibility/proactive-strategy.md | 67 +++++++++---------- .../Proactive/ThresholdExceededArguments.cs | 7 +- .../Proactive/TimingResilienceStrategy.cs | 20 +++--- ...mingResilienceStrategyBuilderExtensions.cs | 16 ++--- .../Proactive/TimingStrategyOptions.cs | 12 ++-- 6 files changed, 60 insertions(+), 71 deletions(-) diff --git a/docs/extensibility/index.md b/docs/extensibility/index.md index 2a73d5a0938..6b7e52b7621 100644 --- a/docs/extensibility/index.md +++ b/docs/extensibility/index.md @@ -17,7 +17,7 @@ Regardless of whether the strategy is reactive or proactive, every new resilienc The strategy options contain properties of following types: -- **Primitive types**: Such as `int`, `bool`, `TimeSpan`, etc. +- **Common types**: Such as `int`, `bool`, `TimeSpan`, etc. - **Delegates**: For example when strategy need to raise an event, or generate a value. In general, the delegates should by asynchronous. - **Arguments**: Used by the delegates to pass the information to consumers. @@ -91,9 +91,8 @@ Arguments are used by individual delegate types to flow information to the consu ```cs -// Arguments-based structs encapsulate information about particular event that occurred inside resilience strategy. -// They cna expose any properties that are relevant to the event. -// For this event the actual duration of execution and the threshold that was exceeded are relevant. +// Structs for arguments encapsulate details about specific events within the resilience strategy. +// Relevant properties to the event can be exposed. In this event, the actual execution time and the exceeded threshold are included. public readonly struct ThresholdExceededArguments { public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, TimeSpan duration) @@ -107,7 +106,7 @@ public readonly struct ThresholdExceededArguments public TimeSpan Duration { get; } - // By convention, all arguments should expose the "Context" property. + // As per convention, all arguments should provide a "Context" property. public ResilienceContext Context { get; } } ``` diff --git a/docs/extensibility/proactive-strategy.md b/docs/extensibility/proactive-strategy.md index 6ac8bf3b3d1..1434f2a6daa 100644 --- a/docs/extensibility/proactive-strategy.md +++ b/docs/extensibility/proactive-strategy.md @@ -8,14 +8,12 @@ Proactive resilience strategies are derived from the [`ResilienceStrategy`](xref ```cs -// The strategies should be internal and not exposed as part of the library's public API. -// The configuration of strategy should be done via extension methods and options. +// Strategies should be internal and not exposed in the library's public API. +// Configure the strategy through extension methods and options. internal sealed class TimingResilienceStrategy : ResilienceStrategy { private readonly TimeSpan _threshold; - private readonly Func? _thresholdExceeded; - private readonly ResilienceStrategyTelemetry _telemetry; public TimingResilienceStrategy( @@ -35,19 +33,19 @@ internal sealed class TimingResilienceStrategy : ResilienceStrategy { var stopwatch = Stopwatch.StartNew(); - // Execute the provided callback and respect the value of ContinueOnCapturedContext property. + // Execute the given callback and adhere to the ContinueOnCapturedContext property value. Outcome outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); if (stopwatch.Elapsed > _threshold) { - // Create arguments that encapsulate the information about the event. + // Bundle information about the event into arguments. var args = new ThresholdExceededArguments(context, _threshold, stopwatch.Elapsed); - // Since we detected that this execution took longer than the threshold, we will report this as an resilience event. + // Report this as a resilience event if the execution took longer than the threshold. _telemetry.Report( - new ResilienceEvent(ResilienceEventSeverity.Warning, "ExecutionThresholdExceeded"), // Pass the event severity and the event name - context, // Forward the context - args); // Forward the arguments so any listeners can recognize this particular event + new ResilienceEvent(ResilienceEventSeverity.Warning, "ExecutionThresholdExceeded"), + context, + args); if (_thresholdExceeded is not null) { @@ -55,7 +53,7 @@ internal sealed class TimingResilienceStrategy : ResilienceStrategy } } - // Just return the outcome + // Return the outcome directly. return outcome; } } @@ -66,9 +64,8 @@ Review the code and comments to understand the implementation. Take note of the ```cs -// Arguments-based structs encapsulate information about particular event that occurred inside resilience strategy. -// They cna expose any properties that are relevant to the event. -// For this event the actual duration of execution and the threshold that was exceeded are relevant. +// Structs for arguments encapsulate details about specific events within the resilience strategy. +// Relevant properties to the event can be exposed. In this event, the actual execution time and the exceeded threshold are included. public readonly struct ThresholdExceededArguments { public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, TimeSpan duration) @@ -82,7 +79,7 @@ public readonly struct ThresholdExceededArguments public TimeSpan Duration { get; } - // By convention, all arguments should expose the "Context" property. + // As per convention, all arguments should provide a "Context" property. public ResilienceContext Context { get; } } ``` @@ -94,7 +91,7 @@ Arguments should always have an `Arguments` suffix and include a `Context` prope In the previous section, we implemented the `TimingResilienceStrategy`. Now, it's time to integrate it with Polly and its public API. -Let's define the public `TimingResilienceStrategy` to configure our strategy: +Let's define the public `TimingStrategyOptions` to configure our strategy: ```cs @@ -102,20 +99,18 @@ public class TimingStrategyOptions : ResilienceStrategyOptions { public TimingStrategyOptions() { - // It's recommended to set the default name for the options so - // the consumer can get additional information in the telemetry. + // Assign a default name to the options for more detailed telemetry insights. Name = "Timing"; } - // You can use the validation attributes to ensure the options are valid. - // The validation will be performed automatically when building the pipeline. + // Apply validation attributes to guarantee the options' validity. + // The pipeline will handle validation automatically during its construction. [Range(typeof(TimeSpan), "00:00:00", "1.00:00:00")] [Required] public TimeSpan? Threshold { get; set; } - // Expose the delegate that will be invoked when the threshold is exceeded. - // The recommendation is that the arguments should have the same name as the delegate but with "Arguments" suffix. - // Notice that the delegate is not required. + // Provide the delegate to be called when the threshold is surpassed. + // Ideally, arguments should share the delegate's name, but with an "Arguments" suffix. public Func? ThresholdExceeded { get; set; } } ``` @@ -125,32 +120,32 @@ Options represent our public contract with the consumer. By using them, we can e ## Extensions -So far, we have: +So far, we've covered: -- Public `TimingResilienceStrategy` and the public arguments associated with them. -- Our proactive strategy implementation - `TimingResilienceStrategy`. +- The public `TimingStrategyOptions` and its associated arguments. +- The proactive strategy implementation named `TimingResilienceStrategy`. -The last step is to combine these components by introducing new extensions for the `ResiliencePipelineBuilder` and `ResiliencePipelineBuilder`. As both builders share the same base class, we can present a single extension for `ResiliencePipelineBuilderBase` to cater to both. +The final step is to integrate these components by adding new extensions for both `ResiliencePipelineBuilder` and `ResiliencePipelineBuilder`. Since both builders inherit from the same base class, we can introduce a single extension for `ResiliencePipelineBuilderBase` to serve both. ```cs public static class TimingResilienceStrategyBuilderExtensions { - // The extensions should return the builder for fluent API. - // For proactive strategies we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder" - // by using generic constraints. + // The extensions should return the builder to support a fluent API. + // For proactive strategies, we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder" + // using generic constraints. public static TBuilder AddTiming(this TBuilder builder, TimingStrategyOptions options) where TBuilder : ResiliencePipelineBuilderBase { - // The strategy should be added via AddStrategy method that accepts a factory delegate - // and validates the options automatically. + // Add the strategy through the AddStrategy method. This method accepts a factory delegate + // and automatically validates the options. return builder.AddStrategy( context => { - // The "context" contains various properties that can be used by the strategy. - // Here, we just use the "Telemetry" and pass it to the strategy. - // The Threshold and ThresholdExceeded is passed from the options. + // The "context" provides various properties for the strategy's use. + // In this case, we simply use the "Telemetry" and pass it to the strategy. + // The Threshold and ThresholdExceeded values are sourced from the options. var strategy = new TimingResilienceStrategy( options.Threshold!.Value, options.ThresholdExceeded, @@ -170,4 +165,4 @@ For further understanding of proactive resilience strategies, consider exploring - [Timing strategy sample](https://github.com/App-vNext/Polly/tree/main/samples/Extensibility/Proactive): A practical example from this guide. - [Timeout resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.Core/Timeout): Discover the built-in timeout resilience strategy implementation. -- [Rate limiter resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.RateLimiting): DIscover how rate limiter strategy is implemented. +- [Rate limiter resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.RateLimiting): Discover how rate limiter strategy is implemented. diff --git a/samples/Extensibility/Proactive/ThresholdExceededArguments.cs b/samples/Extensibility/Proactive/ThresholdExceededArguments.cs index 5b8543a6a7f..dd6d6749db2 100644 --- a/samples/Extensibility/Proactive/ThresholdExceededArguments.cs +++ b/samples/Extensibility/Proactive/ThresholdExceededArguments.cs @@ -4,9 +4,8 @@ namespace Extensibility.Proactive; #region ext-proactive-args -// Arguments-based structs encapsulate information about particular event that occurred inside resilience strategy. -// They cna expose any properties that are relevant to the event. -// For this event the actual duration of execution and the threshold that was exceeded are relevant. +// Structs for arguments encapsulate details about specific events within the resilience strategy. +// Relevant properties to the event can be exposed. In this event, the actual execution time and the exceeded threshold are included. public readonly struct ThresholdExceededArguments { public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, TimeSpan duration) @@ -20,7 +19,7 @@ public ThresholdExceededArguments(ResilienceContext context, TimeSpan threshold, public TimeSpan Duration { get; } - // By convention, all arguments should expose the "Context" property. + // As per convention, all arguments should provide a "Context" property. public ResilienceContext Context { get; } } diff --git a/samples/Extensibility/Proactive/TimingResilienceStrategy.cs b/samples/Extensibility/Proactive/TimingResilienceStrategy.cs index 4f5dfb940df..2f3459733a6 100644 --- a/samples/Extensibility/Proactive/TimingResilienceStrategy.cs +++ b/samples/Extensibility/Proactive/TimingResilienceStrategy.cs @@ -6,14 +6,12 @@ namespace Extensibility.Proactive; #region ext-proactive-strategy -// The strategies should be internal and not exposed as part of the library's public API. -// The configuration of strategy should be done via extension methods and options. +// Strategies should be internal and not exposed in the library's public API. +// Configure the strategy through extension methods and options. internal sealed class TimingResilienceStrategy : ResilienceStrategy { private readonly TimeSpan _threshold; - private readonly Func? _thresholdExceeded; - private readonly ResilienceStrategyTelemetry _telemetry; public TimingResilienceStrategy( @@ -33,19 +31,19 @@ protected override async ValueTask> ExecuteCore outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); if (stopwatch.Elapsed > _threshold) { - // Create arguments that encapsulate the information about the event. + // Bundle information about the event into arguments. var args = new ThresholdExceededArguments(context, _threshold, stopwatch.Elapsed); - // Since we detected that this execution took longer than the threshold, we will report this as an resilience event. + // Report this as a resilience event if the execution took longer than the threshold. _telemetry.Report( - new ResilienceEvent(ResilienceEventSeverity.Warning, "ExecutionThresholdExceeded"), // Pass the event severity and the event name - context, // Forward the context - args); // Forward the arguments so any listeners can recognize this particular event + new ResilienceEvent(ResilienceEventSeverity.Warning, "ExecutionThresholdExceeded"), + context, + args); if (_thresholdExceeded is not null) { @@ -53,7 +51,7 @@ protected override async ValueTask> ExecuteCore" - // by using generic constraints. + // The extensions should return the builder to support a fluent API. + // For proactive strategies, we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder" + // using generic constraints. public static TBuilder AddTiming(this TBuilder builder, TimingStrategyOptions options) where TBuilder : ResiliencePipelineBuilderBase { - // The strategy should be added via AddStrategy method that accepts a factory delegate - // and validates the options automatically. + // Add the strategy through the AddStrategy method. This method accepts a factory delegate + // and automatically validates the options. return builder.AddStrategy( context => { - // The "context" contains various properties that can be used by the strategy. - // Here, we just use the "Telemetry" and pass it to the strategy. - // The Threshold and ThresholdExceeded is passed from the options. + // The "context" provides various properties for the strategy's use. + // In this case, we simply use the "Telemetry" and pass it to the strategy. + // The Threshold and ThresholdExceeded values are sourced from the options. var strategy = new TimingResilienceStrategy( options.Threshold!.Value, options.ThresholdExceeded, diff --git a/samples/Extensibility/Proactive/TimingStrategyOptions.cs b/samples/Extensibility/Proactive/TimingStrategyOptions.cs index 6f449372826..3fa5586a911 100644 --- a/samples/Extensibility/Proactive/TimingStrategyOptions.cs +++ b/samples/Extensibility/Proactive/TimingStrategyOptions.cs @@ -9,20 +9,18 @@ public class TimingStrategyOptions : ResilienceStrategyOptions { public TimingStrategyOptions() { - // It's recommended to set the default name for the options so - // the consumer can get additional information in the telemetry. + // Assign a default name to the options for more detailed telemetry insights. Name = "Timing"; } - // You can use the validation attributes to ensure the options are valid. - // The validation will be performed automatically when building the pipeline. + // Apply validation attributes to guarantee the options' validity. + // The pipeline will handle validation automatically during its construction. [Range(typeof(TimeSpan), "00:00:00", "1.00:00:00")] [Required] public TimeSpan? Threshold { get; set; } - // Expose the delegate that will be invoked when the threshold is exceeded. - // The recommendation is that the arguments should have the same name as the delegate but with "Arguments" suffix. - // Notice that the delegate is not required. + // Provide the delegate to be called when the threshold is surpassed. + // Ideally, arguments should share the delegate's name, but with an "Arguments" suffix. public Func? ThresholdExceeded { get; set; } }