The resilience demo is a proof of concept (POC) showcase of how the unified and non-allocating resilience API can look like.
At the heart of the POC is the IResilienceStrategy
interface that is responsible for execution of user code. It's one interface that handles all Polly scenarios:
ISyncPolicy
IAsyncPolicy
ISyncPolicy<T>
IAsyncPolicy<T>
public interface IResilienceStrategy
{
ValueTask<T> ExecuteAsync<T, TState>(Func<ResilienceContext, TState, ValueTask<T>> execution, ResilienceContext context, TState state);
}
The ResilienceContext
is defined as:
public class ResilienceContext
{
public CancellationToken CancellationToken { get; set; }
public bool IsSynchronous { get; set; }
public bool IsVoid { get; set; }
public bool ContinueOnCapturedContext { get; set; }
// omitted for simplicity
}
The IResilienceStrategy
unifies the 4 different policies used in Polly. User actions are executed under a single API. The are many extension
methods for this interface that cover different scenarios:
- Synchronous void methods.
- Synchronous methods with result.
- Asynchronous void methods.
- Asynchronous methods with result.
For example, synchronous Execute
extension:
public static void Execute(this IResilienceStrategy strategy, Action execute)
{
var context = ResilienceContext.Get();
context.IsSynchronous = true;
context.IsVoid = true;
try
{
strategy.ExecuteAsync(static (context, state) =>
{
state();
return new ValueTask<VoidResult>(VoidResult.Instance);
},
context, execute).GetAwaiter().GetResult();
}
finally
{
ResilienceContext.Return(context);
}
}
In the preceding example:
- We rent
ResilienceContext
from pool. - We store the information about the execution mode by setting the
IsSynchronous
andIsVoid
properties to the context. - We pass the user delegate, and use the
State
to avoid closure allocation. - We block the execution.
- We return
ResilienceContext
to the pool.
Underlying implementation decides how to execute this delegate by reading the ResilienceContext
:
internal class DelayStrategy : DelegatingResilienceStrategy
{
public async override ValueTask<T> ExecuteAsync<T, TState>(Func<ResilienceContext, TState, ValueTask<T>> execution, ResilienceContext context, TState state)
{
if (context.IsSynchronous)
{
Thread.Sleep(1000);
}
else
{
await Task.Delay(1000);
}
return await execution(context, state);
}
}
In the preceding example:
- For synchronous execution we are using
Thread.Sleep
. - For asynchronous execution we are using
Task.Delay
.
This way, the responsibility of how to execute method is lifted from the user and instead passed to the policy. User knows only the IResilienceStrategy
interface. User uses only a single strategy to execute all scenarios. Previously, user had to decide whether to use sync vs async, typed vs non-typed policies.
The life of extensibility author is also simplified as he only maintains one implementation of strategy instead of multiple ones. See the duplications in Polly.Retry
.
This API exposes the following classes and interfaces that can be used to create the resilience strategy:
IResilienceStrategyBuilder
ResilienceStrategyBuilder
: concrete implementation ofIResilienceStrategyBuilder
.
public interface IResilienceStrategyBuilder
{
ResilienceStrategyBuilderProperties Properties { get; set; }
IResilienceStrategyBuilder AddStrategy(IResilienceStrategy strategy, ResilienceStrategyProperties? properties = null);
IResilienceStrategyBuilder AddStrategy(Func<ResilienceStrategyBuilderContext, IResilienceStrategy> factory, ResilienceStrategyProperties? properties = null);
IResilienceStrategy Create(ResilienceStrategyInstanceProperties? properties = null);
}
To create a strategy you chain various extensions for IResilienceStrategyBuilder
followed by the Create
call:
Single strategy:
var resilienceStrategy = new ResilienceStrategyBuilder().AddRetry().Create();
Pipeline of strategies:
var resilienceStrategy = new ResilienceStrategyBuilder()
.AddRetry()
.AddCircuitBreaker()
.AddTimeout(options => { ... })
.Create();
The resilience extensibility is simple. You just expose new extensions for IResilienceStrategyBuilder
that use the IResilienceStrategyBuilder.AddStrategy
methods.
The resilience strategy can handle many different result types and exceptions as retry strategy sample demonstrates:
var options = new RetryStrategyOptions();
options
.ShouldRetry
.Add<HttpResponseMessage>(m => m.StatusCode == HttpStatusCode.InternalServerError) // inspecting the result
.Add(HttpStatusCode.InternalServerError) // particular value for other type
.Add<MyResult>(v => v.IsError)
.Add<MyResult>((v, context) => IsError(context)) // retrieve data from context for evaluation
.AddException<InvalidOperationException>() // exceptions
.AddException<HttpRequestMessageException>() // more exceptions
.Add<MyResult>((v, context) => await IsErrorAsync(v, context)); // async predicates
In the preceding sample retry strategy handles 3 different types of results. This allows sharing of the retry strategy across the different result types. It is also possible to retrieve additional details from the ResilienceContext
when handling the result.
The POC exposes the following resilience packages:
Resilience.Abstractions
: containsIResilienceStrategy
+ extensions.Resilience
: contains implementations,IResiliencePipelineBuilder
+ extensions.Resilience.Polly
: contains extensions and integration points with Polly.Resilience.Strategies
: contains implementations of built-in strategies (retry, bulkhead, timeout, hedging).Resilience.DependencyInjection
: DI support.