Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Docs] General extensibility and implementation of proactive strategies #1602

Merged
merged 6 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions docs/advanced/extensibility.md

This file was deleted.

121 changes: 121 additions & 0 deletions docs/extensibility/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# 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:

- **Primitive types**: Such as `int`, `bool`, `TimeSpan`, etc.
martintmk marked this conversation as resolved.
Show resolved Hide resolved
- **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
// Arguments-based structs encapsulate information about particular event that occurred inside resilience strategy.
// They cna expose any properties that are relevant to the event.
martintmk marked this conversation as resolved.
Show resolved Hide resolved
// 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; }
}
```
<!-- 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.
173 changes: 173 additions & 0 deletions docs/extensibility/proactive-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# 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
// 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
{
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 provided callback and respect the value of ContinueOnCapturedContext property.
Outcome<TResult> 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;
}
}
```
<!-- endSnippet -->

Review the code and comments to understand the implementation. Take note of the `ThresholdExceededArguments` struct:

<!-- snippet: ext-proactive-args -->
```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; }
}
```
<!-- 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 `TimeoutStrategyOptions` to configure our strategy:

<!-- snippet: ext-proactive-options -->
```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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What additional information? If it's just the name I would probably say "the consumer can see it in the telemetry" if it allows access to more things, an example could be handy to help build a picture.

Copy link
Contributor Author

@martintmk martintmk Sep 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded slightly.

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<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 have:

- Public `TimeoutStrategyOptions` 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<T>`. As both builders share the same base class, we can present a single extension for `ResiliencePipelineBuilderBase` to cater to both.

<!-- snippet: ext-proactive-extensions -->
```cs
public static class TimingResilienceStrategyBuilderExtensions
{
// The extensions should return the builder for fluent API.
// For proactive strategies we can target both "ResiliencePipelineBuilderBase" and "ResiliencePipelineBuilder<T>"
// by using generic constraints.
public static TBuilder AddTiming<TBuilder>(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);
}
}
```
<!-- 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
27 changes: 27 additions & 0 deletions samples/Extensibility/Proactive/ThresholdExceededArguments.cs
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading