-
Notifications
You must be signed in to change notification settings - Fork 69
Add support for policy registry #66
Conversation
| return request.Method == HttpMethod.Get ? | ||
| reg.Get<IAsyncPolicy<HttpResponseMessage>>("regular") : | ||
| reg.Get<IAsyncPolicy<HttpResponseMessage>>("long"); | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the best place to start looking. Let's try to nail down what the configuration experience is like and then let the actual implementation take care of itself 😆
| if (!registry.TryGet<IAsyncPolicy<HttpResponseMessage>>(policyKey, out var policy)) | ||
| { | ||
| policy = registry.Get<IAsyncPolicy>(policyKey).WrapAsync(Policy.NoOpAsync<HttpResponseMessage>()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@reisenberger - there's a lot of tension caused in this design by the desire to support IAsyncPolicy and IAsyncPolicy<HttpResponseMessage>.
For instance if we were to support Func<HttpRequestMessage, IAsyncPolicy<HttpResponseMessage>> and Func<HttpRequestMessage, IAsyncPolicy>, then we end up with a method that's overloaded on the return type of Func<..T>. In general this case gives really bad intellisense and I try to avoid it.
It seems like it's always possible to create a PolicyAsync<T> instead of a PolicyAsync for anything you desire. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a lot of tension caused in this design by the desire to support
IAsyncPolicyandIAsyncPolicy<HttpResponseMessage>.
Supporting only IAsyncPolicy<HttpResponseMessage> would certainly simplify the implementation. With HttpClientFactory there is a better case for supporting only generic policies than with Polly in general, given all calls are in HttpResponseMessage.
A concern is that not supporting non-generic IAsyncPolicy will lead to enquiries of why, eg, .AddPolicyHandler(Policy.TimeoutAsync(10)) doesn't compile. In Polly's API, I opted to take the hit and provide bridging overloads (eg in PolicyWrap), so that from the user's perspective, "everything just worked". 'Traffic-managing' resilience strategies like Timeout and Bulkhead have no interest in returned results, so we opted to ensure that non-generic forms could just be used wherever a generic form might otherwise be expected.
If the decision is that HttpClientFactory only supports IAsyncPolicy<HttpResponseMessage>, documentation should cover this, to reduce user blocks, and enquiries.
Polly could add a helper extension method Policy<TResult> Policy.For<TResult>() (internally, a unary-wrapper, a version of .WrapAsync(Policy.NoOpAsync<HttpResponseMessage>()) without the need for the extra NoOpPolicy<TResult>). This could either help users bridge; or HttpClientFactory provide the extra overloads.
Worth further thought / user input / discussion, as will be key to the user experience.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a discussion this morning about this with @glennc - we're thinking that we want to try removing/not-providing the non-generic methods for the preview release and see if we get feedback.
There's a relatively small delta between .DoThing(Policy.Timeout(...)) and .DoThing(Policy.Timeout<HttpResponseMessage>()). We'd rather require the extra typing than ship partial or complicated support for non-generic policies.
If we get the feedback that this is important then we will do the work to add overloads that directly accept non-generic IAsyncPolicy.
--
I agree that it would be a nice addition to have a simpler way to go from IAsyncPolicy->IAsyncPolicy<>. I suppose that's on you guys if you think it's worth doing. For now I plan to just document how to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a balanced approach
We don't really know whether people will miss the non-generic methods so it's probably fine to leave it out and see who complains.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rynowak Very happy with that call 👍; I agonised over this.
Offering the mix of non-generic and generic overloads makes sense for Polly generally, as we have a range of use cases to support, some of which suit non-generic policies better; but with HttpClientFactory's one use case (for Polly), there is a chance to reduce complexity. Simplicity is a great virtue.
| /// Provides convenience extension methods to register <see cref="IPolicyRegistry{String}"/> and | ||
| /// <see cref="IReadOnlyPolicyRegistry{String}"/> in the service collection. | ||
| /// </summary> | ||
| public static class PollyServiceCollectionExtensions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are just for convenience. Our AddPolicyHandlerFromRegistry methods will require you to have registered IReadOnlyPolicyRegistry<string> somehow. We don't care if you used these methods to do it.
martincostello
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A question about Context and two minor nits, but otherwise this looks nice.
| public static class PollyServiceCollectionExtensions | ||
| { | ||
| /// <summary> | ||
| /// Registers and empty <see cref="PolicyRegistry"/> in the service collection with service types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "Registers an empty."
|
|
||
| return _policy.ExecuteAsync((ct) => base.SendAsync(request, ct), cancellationToken); | ||
| var policy = ResolvePolicy(request); | ||
| return policy.ExecuteAsync((ct) => base.SendAsync(request, ct), cancellationToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be a null-check on policy after ResolvePolicy() returns?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agonized over this and couldn't really come to a good decision. Usually my philosophy is play stupid games, win stupid prizes...
This can be done in a way that doesn't impact the perf of the non-dynamic way so it's probably worth doing to allay confusion.
|
|
||
| return _policy.ExecuteAsync((ct) => base.SendAsync(request, ct), cancellationToken); | ||
| var policy = ResolvePolicy(request); | ||
| return policy.ExecuteAsync((ct) => base.SendAsync(request, ct), cancellationToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does there need to be a mechanic here to provide a way to create a Context which could be passed into the policy, or is that going to be catered for separately/elsewhere? Given that the policy is already being resolved from the request, there could be a hook to resolve a context from the request as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #65
Whomever is the caller can provide a context, or another message handler can do it (since they are also callers).
| return policy.ExecuteAsync((ct) => base.SendAsync(request, ct), cancellationToken); | ||
| } | ||
|
|
||
| private IAsyncPolicy<HttpResponseMessage> ResolvePolicy(HttpRequestMessage request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rynowak this is great, I think it solves most of the things I was concerned about.
reisenberger
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rynowak This looks to fulfil the goals you set out for retrieving policies from PolicyRegistry for HttpClientFactory:
- Support a registry which may dynamically update the configuration of a policy
- Allow policy selection (choosing policy based on request)
Two observations:
- we could extract 'what-to-handle' from its overload, to make it reusable
- the funcs would/should generally select from amongst stable instances (unless config changes), so 'factory' may be misleading?
|
|
||
| public static IHttpClientBuilder AddPolicyHandler( | ||
| this IHttpClientBuilder builder, | ||
| Func<HttpRequestMessage, IAsyncPolicy<HttpResponseMessage>> policyFactory) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest: these parameters be called policySelector (or policyProvider?) throughout. policyFactory may suggest users are expected to new up a policy within the Func.
Selecting a previously-config'd policy from somewhere should usually be the pattern. Per previous discussion, with stateful policies like CircuitBreaker, the Func would need to be returning the same instance consistently, in order to maintain circuit state over time. Only if the config is changing should a new instance be supplied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that sounds fine.
| /// </para> | ||
| /// </remarks> | ||
| public static IHttpClientBuilder AddServerErrorPolicyHandler( | ||
| this IHttpClientBuilder builder, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if AddServerErrorPolicyHandler(...) is starting to feel disjoint from the others now? It fulfils its essence, to encode errors worth handling, but that 'what-to-handle' power is locked in, not available with its sibling 'how-to-handle' overloads.
Option: Extract a method just returning the PolicyBuilder<HttpResponseMessage> policyBuilder (what-to-handle) it defines, say .HandleTransientHttpErrors().
- Users can then use
.HandleTransientHttpErrors()anywhere, including for policies provided by the sibling 'how-to-handle' overloads. - Users can riff on it flexibly, eg:
.HandleTransientHttpErrors().Or<TimeoutRejectedException>()- if they were using PollyTimeoutPolicyon inner tries, and wanted a wrapping retry policy to handle those timeouts..HandleTransientHttpErrors().OrResult(r => r.StatusCode == HttpStatusCode.RequestTimeout)- or whatever, perhaps some custom status code a particular endpoint returns.
Q then becomes where to site this method, as it's a static method. HttpClientFactory.HandleTransientHttpErrors() ??
Option: we could take it back into Polly, where it would sit in an expected place alongside other Handle syntax:
Policy.HandleTransientHttpErrors()
Thoughts?
Variant: If extracting .HandleTransientHttpErrors(), HttpClientFactory could still retain .AddServerErrorPolicyHandler(...) (using the extracted method), if desired to keep that simple usage story within HttpClientFactory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this feedback makes sense. I'm going to consider this out of scope for this PR thought, will address it in a follow up.
|
|
||
| private IAsyncPolicy<HttpResponseMessage> ResolvePolicy(HttpRequestMessage request) | ||
| { | ||
| return _policy ?? _policyFactory(request); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rynowak How do you plan to make to PolicyRegistry accessible from this class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't plan to do that. You provide a lambda that resolves the policy, take a look at the sample and let me know if that won't meet your needs.
Here's a straw man to discuss for the design of retrieving policies from the registry for IHttpClientFactory. Goals: - Keep simple usage simple - Support updates (opt in) - Allow use of policies by name and with code - Allow dynamism (choosing policy based on request) Non-goal: - Avoid overloading based on Func<> types with same arity. This is a destructive case for intellisense and should be avoided.
33ce079 to
30a6862
Compare
|
I've addressed all of the minor feedback here. Planning to add tests tomorrow |
| // Assert - 2 | ||
| Assert.Empty(factory._activeHandlers); | ||
| Assert.True(factory.CleanupTimerStarted.IsSet); | ||
| Assert.True(factory.CleanupTimerStarted.IsSet, "Cleanup timer started"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just misc cleanup that snuck in while I was looking at a flaky test 😆
|
Updated and added tests. I converted the existing builder tests into end-to-ends. |
2804537 to
b268058
Compare
Here's a straw man to discuss for the design of retrieving policies from
the registry for IHttpClientFactory.
Goals:
Non-goal:
destructive case for intellisense and should be avoided.