-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Docs] General extensibility and implementation of proactive strategi…
…es (#1602)
- Loading branch information
Showing
14 changed files
with
501 additions
and
426 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# Extensibility | ||
|
||
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 guide will help you create a new illustrative resilience strategy for each type. | ||
|
||
## Basics of extensibility | ||
|
||
Regardless of whether the strategy is reactive or proactive, every new resilience strategy should include the following components: | ||
|
||
- Options detailing the strategy's configuration. These should inherit from [`ResilienceStrategyOptions`](xref:Polly.ResilienceStrategyOptions). | ||
- Extensions for `ResiliencePipelineBuilder` or `ResiliencePipelineBuilder<T>`. | ||
- Custom argument types for delegates that contain information about a specific event. | ||
|
||
The strategy options contain properties of following types: | ||
|
||
- **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. | ||
|
||
## 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<Args<TResult>, ValueTask<bool>>` (Reactive) | ||
|
||
### Events | ||
|
||
- `Func<Args<TResult>, ValueTask>` (Reactive) | ||
- `Func<Args, ValueTask>` (Proactive) | ||
|
||
### Generators | ||
|
||
- `Func<Args<TResult>, ValueTask<TValue>>` (Reactive) | ||
- `Func<Args, ValueTask<TValue>>` (Proactive) | ||
|
||
These delegates accept either `Args` or `Args<TResult>` 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: | ||
|
||
<!-- snippet: delegate-usage --> | ||
```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<string>() | ||
.AddRetry(new RetryStrategyOptions<string> | ||
{ | ||
// 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(); | ||
``` | ||
<!-- endSnippet --> | ||
|
||
## 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: | ||
|
||
<!-- snippet: ext-proactive-args --> | ||
```cs | ||
// 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) | ||
{ | ||
Context = context; | ||
Threshold = threshold; | ||
Duration = duration; | ||
} | ||
|
||
public TimeSpan Threshold { get; } | ||
|
||
public TimeSpan Duration { get; } | ||
|
||
// As per convention, all arguments should provide a "Context" property. | ||
public ResilienceContext Context { get; } | ||
} | ||
``` | ||
<!-- endSnippet --> | ||
|
||
## 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
# Proactive resilience strategy | ||
|
||
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 are derived from the [`ResilienceStrategy`](xref:Polly.ResilienceStrategy) base class. For this strategy, the implementation is: | ||
|
||
<!-- snippet: ext-proactive-strategy --> | ||
```cs | ||
// 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<ThresholdExceededArguments, ValueTask>? _thresholdExceeded; | ||
private readonly ResilienceStrategyTelemetry _telemetry; | ||
|
||
public TimingResilienceStrategy( | ||
TimeSpan threshold, | ||
Func<ThresholdExceededArguments, ValueTask>? thresholdExceeded, | ||
ResilienceStrategyTelemetry telemetry) | ||
{ | ||
_threshold = threshold; | ||
_telemetry = telemetry; | ||
_thresholdExceeded = thresholdExceeded; | ||
} | ||
|
||
protected override async ValueTask<Outcome<TResult>> ExecuteCore<TResult, TState>( | ||
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback, | ||
ResilienceContext context, | ||
TState state) | ||
{ | ||
var stopwatch = Stopwatch.StartNew(); | ||
|
||
// Execute the given callback and adhere to the ContinueOnCapturedContext property value. | ||
Outcome<TResult> outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); | ||
|
||
if (stopwatch.Elapsed > _threshold) | ||
{ | ||
// Bundle information about the event into arguments. | ||
var args = new ThresholdExceededArguments(context, _threshold, stopwatch.Elapsed); | ||
|
||
// Report this as a resilience event if the execution took longer than the threshold. | ||
_telemetry.Report( | ||
new ResilienceEvent(ResilienceEventSeverity.Warning, "ExecutionThresholdExceeded"), | ||
context, | ||
args); | ||
|
||
if (_thresholdExceeded is not null) | ||
{ | ||
await _thresholdExceeded(args).ConfigureAwait(context.ContinueOnCapturedContext); | ||
} | ||
} | ||
|
||
// Return the outcome directly. | ||
return outcome; | ||
} | ||
} | ||
``` | ||
<!-- endSnippet --> | ||
|
||
Review the code and comments to understand the implementation. Take note of the `ThresholdExceededArguments` struct: | ||
|
||
<!-- snippet: ext-proactive-args --> | ||
```cs | ||
// 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) | ||
{ | ||
Context = context; | ||
Threshold = threshold; | ||
Duration = duration; | ||
} | ||
|
||
public TimeSpan Threshold { get; } | ||
|
||
public TimeSpan Duration { get; } | ||
|
||
// As per convention, all arguments should provide a "Context" property. | ||
public ResilienceContext Context { get; } | ||
} | ||
``` | ||
<!-- endSnippet --> | ||
|
||
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 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 `TimingStrategyOptions` to configure our strategy: | ||
|
||
<!-- snippet: ext-proactive-options --> | ||
```cs | ||
public class TimingStrategyOptions : ResilienceStrategyOptions | ||
{ | ||
public TimingStrategyOptions() | ||
{ | ||
// Assign a default name to the options for more detailed telemetry insights. | ||
Name = "Timing"; | ||
} | ||
|
||
// 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; } | ||
|
||
// 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<ThresholdExceededArguments, ValueTask>? ThresholdExceeded { get; set; } | ||
} | ||
``` | ||
<!-- endSnippet --> | ||
|
||
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 | ||
|
||
So far, we've covered: | ||
|
||
- The public `TimingStrategyOptions` and its associated arguments. | ||
- The proactive strategy implementation named `TimingResilienceStrategy`. | ||
|
||
The final step is to integrate these components by adding new extensions for both `ResiliencePipelineBuilder` and `ResiliencePipelineBuilder<T>`. Since both builders inherit from the same base class, we can introduce a single extension for `ResiliencePipelineBuilderBase` to serve both. | ||
|
||
<!-- snippet: ext-proactive-extensions --> | ||
```cs | ||
public static class TimingResilienceStrategyBuilderExtensions | ||
{ | ||
// The extensions should return the builder to support a fluent API. | ||
// For proactive strategies, we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder<T>" | ||
// using generic constraints. | ||
public static TBuilder AddTiming<TBuilder>(this TBuilder builder, TimingStrategyOptions options) | ||
where TBuilder : ResiliencePipelineBuilderBase | ||
{ | ||
// Add the strategy through the AddStrategy method. This method accepts a factory delegate | ||
// and automatically validates the options. | ||
return builder.AddStrategy( | ||
context => | ||
{ | ||
// 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, | ||
context.Telemetry); | ||
|
||
return strategy; | ||
}, | ||
options); | ||
} | ||
} | ||
``` | ||
<!-- endSnippet --> | ||
|
||
## 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): 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Reactive resilience strategy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
samples/Extensibility/Proactive/ThresholdExceededArguments.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
using Polly; | ||
|
||
namespace Extensibility.Proactive; | ||
|
||
#region ext-proactive-args | ||
|
||
// 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) | ||
{ | ||
Context = context; | ||
Threshold = threshold; | ||
Duration = duration; | ||
} | ||
|
||
public TimeSpan Threshold { get; } | ||
|
||
public TimeSpan Duration { get; } | ||
|
||
// As per convention, all arguments should provide a "Context" property. | ||
public ResilienceContext Context { get; } | ||
} | ||
|
||
#endregion |
Oops, something went wrong.