Observations about v8 API
See original GitHub issueI’ve started to scrutinize the V8 samples and I have found the following things:
Readme
The links are broken. They are pointing to the wrong places.
Intro
strategy = new ResilienceStrategyBuilder()
// Add retries using the options
.AddRetry(new RetryStrategyOptions
{
ShouldRetry = outcome =>
{
// We want to retry on this specific exception
if (outcome.Exception is TimeoutRejectedException)
{
// The "PredicateResult.True" is shorthand to "new ValueTask<bool>(true)"
return PredicateResult.True;
}
return PredicateResult.False;
},
This ShouldRetry
feels a bit clumsy. IMHO it should be as simple as this:
ShouldRetry = outcome => outcome.Exception is TimeoutRejectedException,
The shortest version which can be used now still feels clumsy a bit
ShouldRetry = outcome => new ValueTask<bool>(outcome.Exception is TimeoutRejectedException),
GenericStrategies
ResilienceStrategy<HttpResponseMessage> strategy = new ResilienceStrategyBuilder<HttpResponseMessage>()
.AddFallback(new FallbackStrategyOptions<HttpResponseMessage>
{
FallbackAction = async _ =>
{
await Task.Delay(10);
// Return fallback result
return new Outcome<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.OK));
},
If I want to simply return with 200 (without any async op) then I still have to write this code
FallbackAction = _ => new ValueTask<Outcome<HttpResponseMessage>>(new Outcome<HttpResponseMessage>(new HttpResponseMessage(HttpStatusCode.OK))),
Another thing is that the AddTimeout
does not have any impact on the to-be-decorated method in the example.
For example if I increase the Task.Delay(10)
to Task.Delay(10_000)
still the timeout never triggers.
By passing the token
to the Task.Delay
causes a timeout.
I would suggest to either remove the Timeout policy or fix the example to work with timeout scenario as well.
Retries
Based on the samples I can define a retry strategy like this:
strategy = new ResilienceStrategyBuilder()
.AddRetry(new RetryStrategyOptions
{
ShouldRetry = outcome => new ValueTask<bool>(outcome.Exception is InvalidOperationException),
RetryCount = 4,
RetryDelayGenerator = outcome => new ValueTask<TimeSpan>(TimeSpan.FromSeconds(outcome.Arguments.Attempt)),
BaseDelay = TimeSpan.FromSeconds(10),
BackoffType = RetryBackoffType.Constant, //or Exponential or any value
OnRetry = outcome =>
{
Console.WriteLine($"Retrying delay: {outcome.Arguments.RetryDelay}");
return default;
}
})
.Build();
strategy.Execute(() => throw new InvalidOperationException());
It does compile and it does perform retries. In the above example the BaseDelay
and BackoffType
values are ignored.
Extensibility
I do understand the intend to do not expose the strategy itself rather just its options.
Still it feels like you have to write a lots of code to make it work.
It feels a bit weird for me that one of the overloads of the AddStrategy
returns with a ResilienceStrategyBuilder
, but the other overload returns void
. In the former case you have to call .Build
in the latter you can’t.
Also IMHO the ResilienceStrategyBuilder
should allow just a single strategy registration. But currently you can register as many you want. What is the use case where you would need multiple strategies?
var strategy = new ResilienceStrategyBuilder()
.AddStrategy(new MySimpleStrategy())
.AddMyResilienceStrategy(new MyResilienceStrategyOptions
{
OnCustomEvent = args => { Console.WriteLine("OnCustomEvent"); return default; }
})
.AddStrategy(new MySimpleStrategy())
.Build();
DependencyInjection
var serviceProvider = new ServiceCollection()
.AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug))
.AddResilienceStrategy("my-strategy", (builder, context) =>
{
context.ServiceProvider.GetRequiredService<ILoggerFactory>();
builder.AddTimeout(TimeSpan.FromSeconds(1));
})
.AddResilienceStrategy<string, HttpResponseMessage>("my-strategy", builder =>
{
builder.AddTimeout(TimeSpan.FromSeconds(1));
})
.BuildServiceProvider();
Why can I register multiple strategies with same name/key? Even I can register the exact same strategy multiple times
var serviceProvider = new ServiceCollection()
.AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug))
.AddResilienceStrategy("my-strategy", (builder, context) =>
{
context.ServiceProvider.GetRequiredService<ILoggerFactory>();
builder.AddTimeout(TimeSpan.FromSeconds(1));
})
.AddResilienceStrategy("my-strategy", (builder, context) =>
{
context.ServiceProvider.GetRequiredService<ILoggerFactory>();
builder.AddTimeout(TimeSpan.FromSeconds(1));
})
.BuildServiceProvider();
Why the strategy
have a type like this: Polly.Utils.ResilienceStrategyPipeline
, whereas the genericStrategy
is Polly.ResilienceStrategy<HttpResponseMessage>
?
Issue Analytics
- State:
- Created 3 months ago
- Reactions:1
- Comments:9 (5 by maintainers)
Top GitHub Comments
Hey @martintmk, I’ve just checked it and yes you have addressed almost all of them (except the naming of
ResilienceStrategyBuilder
). Thank you Sir! 😃Hey @PeterCsalaHbo
Thanks for your feedback!
Intro
Polly V8 is from async ground-up, this also makes all predicates asynchronous as well. There are scenarios where you want to determine whether to retry or not by buffering the HTTP response body which is an async operation.
We are thinking of simplifying the setup by using PredicateBuilder.
What do you think about this syntax?
Basically,
WithShouldRetry
would be just convenience extension forRetryStrategyOptions
that allows simplified configuration of predicates.GenericStrategies
I think the same thing as before, we can simplify this by exposing convenience extensions for either
FallbackStrategyOptions<HttpResponseMessage>
or some additional fallback extensions forResilienceStrategyBuilder
Actually, at some point we had some fluent API for building the predicates the easy way. However, it was deemed to complex for what it does and dropped in favor of simple delegates.
Callback API: #1067 Move to delegates : #1211
Extensibility
It’s the recommended approach, you can still just inline the strategy for simple scenarios (or make it public). But official strategies are internal and kept as implementation detail. This reduces the overal API surface. The actual configuration of strategies happens through extensions and options.
The problem is that
AddStrategy
that returns void is an instance member, while the other is extension. However, I think we can make it work if we turn it to extension too.This was one of the core ideas behind V8, there are no individual strategies anymore, everything is
ResilienceStrategy
and you don’t care of how many strategies is it composed of. The configuration of pipeline is done during the setup ofResilienceStrategyBuilder
. (it’s basically the V7 policy wrap, but it’s now built-in).We also promote the strategy composition as for any serious resilience patterns you have more than one strategy in the pipeline.
Note that if you add only a single strategy to the builder the returning instance will be the same as the one you added to it:
DependencyInjection
This enables the extensibility and customization. Basically, the last added strategy wins and overwrites the previous one. This allows customization of resilience strategies in the libraries even if they don’t expose dedicated API for customization. Open to ideas here.
Thanks again for the feedback, I can address some of those soon 😃