Skip to content

Commit

Permalink
[Docs] General extensibility and implementation of proactive strategi…
Browse files Browse the repository at this point in the history
…es (#1602)
  • Loading branch information
martintmk committed Sep 21, 2023
1 parent 5af6eb0 commit cc01f38
Show file tree
Hide file tree
Showing 14 changed files with 501 additions and 426 deletions.
3 changes: 0 additions & 3 deletions docs/advanced/extensibility.md

This file was deleted.

120 changes: 120 additions & 0 deletions docs/extensibility/index.md
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.
168 changes: 168 additions & 0 deletions docs/extensibility/proactive-strategy.md
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.
1 change: 1 addition & 0 deletions docs/extensibility/reactive-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Reactive resilience strategy
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 9 additions & 2 deletions docs/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions samples/Extensibility/Proactive/ThresholdExceededArguments.cs
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
Loading

0 comments on commit cc01f38

Please sign in to comment.