Skip to content

Commit

Permalink
[Docs] Reactive strategies extensibility (#1606)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Sep 21, 2023
1 parent 83700e8 commit 0885077
Show file tree
Hide file tree
Showing 11 changed files with 507 additions and 11 deletions.
23 changes: 21 additions & 2 deletions docs/extensibility/proactive-strategy.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 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.
This document 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

Expand Down Expand Up @@ -139,7 +139,6 @@ public static class TimingResilienceStrategyBuilderExtensions
{
// Add the strategy through the AddStrategy method. This method accepts a factory delegate
// and automatically validates the options.
return builder.AddStrategy(
context =>
{
Expand All @@ -159,6 +158,26 @@ public static class TimingResilienceStrategyBuilderExtensions
```
<!-- endSnippet -->

## Usage

<!-- snippet: ext-proactive-strategy-usage -->
```cs
// Add the proactive strategy to the builder
var pipeline = new ResiliencePipelineBuilder()
// This is custom extension defined in this sample
.AddTiming(new TimingStrategyOptions
{
Threshold = TimeSpan.FromSeconds(1),
ThresholdExceeded = args =>
{
Console.WriteLine("Execution threshold exceeded!");
return default;
},
})
.Build();
```
<!-- endSnippet -->

## Resources

For further understanding of proactive resilience strategies, consider exploring these resources:
Expand Down
249 changes: 249 additions & 0 deletions docs/extensibility/reactive-strategy.md
Original file line number Diff line number Diff line change
@@ -1 +1,250 @@
# Reactive resilience strategy

This document describes how to set up a **Result reporting resilience strategy**. This strategy lets you listen for specific results and report them to other components. It serves as a good example of a reactive strategy because it deals with specific results.

## Implementation

Reactive resilience strategies inherit from the [`ResilienceStrategy<T>`](xref:Polly.ResilienceStrategy`1) base class. The implementation for this specific strategy is as follows:

<!-- snippet: ext-reactive-strategy -->
```cs
// Strategies should be internal and not exposed in the library's public API.
// Use extension methods and options to configure the strategy.
internal sealed class ResultReportingResilienceStrategy<T> : ResilienceStrategy<T>
{
private readonly Func<ResultReportingPredicateArguments<T>, ValueTask<bool>> _shouldHandle;
private readonly Func<OnReportResultArguments<T>, ValueTask> _onReportResult;
private readonly ResilienceStrategyTelemetry _telemetry;

public ResultReportingResilienceStrategy(
Func<ResultReportingPredicateArguments<T>, ValueTask<bool>> shouldHandle,
Func<OnReportResultArguments<T>, ValueTask> onReportResult,
ResilienceStrategyTelemetry telemetry)
{
_shouldHandle = shouldHandle;
_onReportResult = onReportResult;
_telemetry = telemetry;
}

protected override async ValueTask<Outcome<T>> ExecuteCore<TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<T>>> callback,
ResilienceContext context,
TState state)
{
Outcome<T> outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext);

// Check if the outcome should be reported using the "ShouldHandle" predicate.
if (await _shouldHandle(new ResultReportingPredicateArguments<T>(context, outcome)).ConfigureAwait(context.ContinueOnCapturedContext))
{
var args = new OnReportResultArguments<T>(context, outcome);

// Report the event with an informational severity level to the telemetry infrastructure.
_telemetry.Report(new ResilienceEvent(ResilienceEventSeverity.Information, "ResultReported"), context, outcome, args);

// Call the "OnReportResult" callback.
await _onReportResult(args).ConfigureAwait(context.ContinueOnCapturedContext);
}

return outcome;
}
}
```
<!-- endSnippet -->

Reactive strategies use the `ShouldHandle` predicate to decide whether to handle the outcome of a user callback. The convention is to name the predicate's arguments using the `<StrategyName>PredicateArguments` pattern and return a `ValueTask<bool>`. Here, we use `ResultReportingPredicateArguments<TResult>`:

<!-- snippet: ext-reactive-predicate-args -->
```cs
public struct ResultReportingPredicateArguments<TResult>
{
public ResultReportingPredicateArguments(ResilienceContext context, Outcome<TResult> outcome)
{
Context = context;
Outcome = outcome;
}

// Always include the "Context" property in the arguments.
public ResilienceContext Context { get; }

// Always have the "Outcome" property in reactive arguments.
public Outcome<TResult> Outcome { get; }
}
```
<!-- endSnippet -->

Reactive arguments always contain the `Context` and `Outcome` properties.

Additionally, to report the outcome, the strategy uses `OnReportResultArguments<TResult>`:

<!-- snippet: ext-reactive-event-args -->
```cs
public struct OnReportResultArguments<TResult>
{
public OnReportResultArguments(ResilienceContext context, Outcome<TResult> outcome)
{
Context = context;
Outcome = outcome;
}

// Always include the "Context" property in the arguments.
public ResilienceContext Context { get; }

// Always have the "Outcome" property in reactive arguments.
public Outcome<TResult> Outcome { get; }
}
```
<!-- endSnippet -->

Using arguments in callbacks supports a more maintainable and extensible API.

## Options

In the previous section, we implemented the `ResultReportingResilienceStrategy<T>`. Now, we need to integrate it with Polly and its public API.

Define the public `ResultReportingStrategyOptions<TResult>` to configure our strategy:

<!-- snippet: ext-reactive-options -->
```cs
public class ResultReportingStrategyOptions<TResult> : ResilienceStrategyOptions
{
public ResultReportingStrategyOptions()
{
// Set a default name for the options to enhance telemetry insights.
Name = "ResultReporting";
}

// Options for reactive strategies should always include a "ShouldHandle" delegate.
// Set a sensible default when possible. Here, we handle all exceptions.
public Func<ResultReportingPredicateArguments<TResult>, ValueTask<bool>> ShouldHandle { get; set; } = args =>
{
return new ValueTask<bool>(args.Outcome.Exception is not null);
};

// This illustrates an event delegate. Note that the arguments struct carries the same name as the delegate but with an "Arguments" suffix.
// The event follows the async convention and must be set by the user.
//
// The [Required] enforces the consumer to specify this property, used when some properties do not have sensible defaults and are required.
[Required]
public Func<OnReportResultArguments<TResult>, ValueTask>? OnReportResult { get; set; }
}
```
<!-- endSnippet -->

If you want to support non-generic options for the `ResiliencePipelineBuilder`, you can expose them as well:

<!-- snippet: ext-reactive-non-generic-options -->
```cs
// Simply derive from the generic options, using 'object' as the result type.
// This allows the strategy to manage all results.
public class ResultReportingStrategyOptions : ResultReportingStrategyOptions<object>
{
}
```
<!-- endSnippet -->

Using options as a public contract helps us ensure flexibility with consumers. By adopting this method, you can effortlessly introduce new members without inducing breaking changes and maintain consistent validation.

## Extensions

Up until now, we've discussed:

- The public `ResultReportingStrategyOptions<TResult>` and the related arguments.
- The proactive strategy implementation called `ResultReportingResilienceStrategy<TResult>`.

The next step is to combine these elements by introducing new extensions for `ResiliencePipelineBuilder<T>` and, optionally, `ResiliencePipelineBuilder`.

<!-- snippet: ext-reactive-extensions -->
```cs
public static class ResultReportingResilienceStrategyBuilderExtensions
{
// Add extensions for the generic builder.
// Extensions should return the builder to support a fluent API.
public static ResiliencePipelineBuilder<TResult> AddResultReporting<TResult>(this ResiliencePipelineBuilder<TResult> builder, ResultReportingStrategyOptions<TResult> options)
{
// Incorporate the strategy using the AddStrategy method. This method receives a factory delegate
// and automatically checks the options.
return builder.AddStrategy(
context =>
{
// The "context" offers various properties for the strategy to use.
// Here, we simply use the "Telemetry" and hand it over to the strategy.
// The ShouldHandle and OnReportResult values come from the options.
var strategy = new ResultReportingResilienceStrategy<TResult>(
options.ShouldHandle,
options.OnReportResult!,
context.Telemetry);

return strategy;
},
options);
}

// Optionally, if suitable for the strategy, add support for non-generic builders.
// Observe the use of the non-generic ResultReportingStrategyOptions.
public static ResiliencePipelineBuilder AddResultReporting(this ResiliencePipelineBuilder builder, ResultReportingStrategyOptions options)
{
return builder.AddStrategy(
context =>
{
var strategy = new ResultReportingResilienceStrategy<object>(
options.ShouldHandle,
options.OnReportResult!,
context.Telemetry);

return strategy;
},
options);
}
}
```
<!-- endSnippet -->

## Usage

<!-- snippet: ext-reactive-strategy-usage -->
```cs
// Add reactive strategy to the builder
new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddResultReporting(new ResultReportingStrategyOptions<HttpResponseMessage>
{
// Define what outcomes to handle
ShouldHandle = args => args.Outcome switch
{
{ Exception: { } } => PredicateResult.True(),
{ Result: { StatusCode: HttpStatusCode.InternalServerError } } => PredicateResult.True(),
_ => PredicateResult.False()
},
OnReportResult = args =>
{
Console.WriteLine($"Result: {args.Outcome}");
return default;
}
});

// You can also use the non-generic ResiliencePipelineBuilder to handle any kind of result.
new ResiliencePipelineBuilder()
.AddResultReporting(new ResultReportingStrategyOptions
{
// Define what outcomes to handle
ShouldHandle = args => args.Outcome switch
{
{ Exception: { } } => PredicateResult.True(),
{ Result: HttpResponseMessage message } when message.StatusCode == HttpStatusCode.InternalServerError => PredicateResult.True(),
_ => PredicateResult.False()
},
OnReportResult = args =>
{
Console.WriteLine($"Result: {args.Outcome}");
return default;
}
});
```
<!-- endSnippet -->

## Resources

For further understanding of reactive resilience strategies, consider exploring these resources:

- [Result reporting strategy sample](https://github.com/App-vNext/Polly/tree/main/samples/Extensibility/Reactive): A practical example from this guide.
- [Retry resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.Core/Retry): Discover the built-in retry resilience strategy implementation.
- [Fallback resilience strategy](https://github.com/App-vNext/Polly/tree/main/src/Polly.Core/Fallback): Discover the built-in fallback resilience strategy implementation.
4 changes: 2 additions & 2 deletions docs/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ By default, asynchronous continuations and retries do not execute on a captured
ResilienceContext context = ResilienceContextPool.Shared.Get(continueOnCapturedContext: true);

await pipeline.ExecuteAsync(
async context =>
static async context =>
{
// Execute your code, honoring the ContinueOnCapturedContext setting
await MyMethodAsync(context.CancellationToken).ConfigureAwait(context.ContinueOnCapturedContext);
Expand All @@ -51,7 +51,7 @@ The `CancellationToken` you pass to the `ExecuteAsync(...)` method serves multip
```cs
// Execute your code with cancellation support
await pipeline.ExecuteAsync(
async token => await MyMethodAsync(token),
static async token => await MyMethodAsync(token),
cancellationToken);

// Use ResilienceContext for more advanced scenarios
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public static TBuilder AddTiming<TBuilder>(this TBuilder builder, TimingStrategy
{
// Add the strategy through the AddStrategy method. This method accepts a factory delegate
// and automatically validates the options.

return builder.AddStrategy(
context =>
{
Expand Down
51 changes: 47 additions & 4 deletions samples/Extensibility/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using Extensibility.Proactive;
using Extensibility.Reactive;
using Polly;
using System.Net.Http.Headers;
using System.Net;

// ------------------------------------------------------------------------
// Usage of custom proactive strategy
// ------------------------------------------------------------------------
#region ext-proactive-strategy-usage

// Add the proactive strategy to the builder
var pipeline = new ResiliencePipelineBuilder()
// This is custom extension defined in this sample
.AddTiming(new TimingStrategyOptions
Expand All @@ -18,6 +19,48 @@
})
.Build();

#endregion

#region ext-reactive-strategy-usage

// Add reactive strategy to the builder
new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddResultReporting(new ResultReportingStrategyOptions<HttpResponseMessage>
{
// Define what outcomes to handle
ShouldHandle = args => args.Outcome switch
{
{ Exception: { } } => PredicateResult.True(),
{ Result: { StatusCode: HttpStatusCode.InternalServerError } } => PredicateResult.True(),
_ => PredicateResult.False()
},
OnReportResult = args =>
{
Console.WriteLine($"Result: {args.Outcome}");
return default;
}
});

// You can also use the non-generic ResiliencePipelineBuilder to handle any kind of result.
new ResiliencePipelineBuilder()
.AddResultReporting(new ResultReportingStrategyOptions
{
// Define what outcomes to handle
ShouldHandle = args => args.Outcome switch
{
{ Exception: { } } => PredicateResult.True(),
{ Result: HttpResponseMessage message } when message.StatusCode == HttpStatusCode.InternalServerError => PredicateResult.True(),
_ => PredicateResult.False()
},
OnReportResult = args =>
{
Console.WriteLine($"Result: {args.Outcome}");
return default;
}
});

#endregion

// Execute the pipeline
await pipeline.ExecuteAsync(async token => await Task.Delay(1500, token), CancellationToken.None);

Expand Down
Loading

0 comments on commit 0885077

Please sign in to comment.