Skip to content
This repository was archived by the owner on Nov 17, 2018. It is now read-only.

Conversation

@rynowak
Copy link
Member

@rynowak rynowak commented Feb 24, 2018

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.

return request.Method == HttpMethod.Get ?
reg.Get<IAsyncPolicy<HttpResponseMessage>>("regular") :
reg.Get<IAsyncPolicy<HttpResponseMessage>>("long");
})
Copy link
Member Author

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>());
}
Copy link
Member Author

@rynowak rynowak Feb 24, 2018

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?

Copy link
Contributor

@reisenberger reisenberger Feb 25, 2018

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 IAsyncPolicy and IAsyncPolicy<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.

Copy link
Member Author

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.

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.

Copy link
Contributor

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.

@rynowak
Copy link
Member Author

rynowak commented Feb 24, 2018

/// Provides convenience extension methods to register <see cref="IPolicyRegistry{String}"/> and
/// <see cref="IReadOnlyPolicyRegistry{String}"/> in the service collection.
/// </summary>
public static class PollyServiceCollectionExtensions
Copy link
Member Author

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.

Copy link
Contributor

@martincostello martincostello left a 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
Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Member Author

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);
Copy link
Contributor

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.

Copy link
Member Author

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)

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.

Copy link
Contributor

@reisenberger reisenberger left a 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)
Copy link
Contributor

@reisenberger reisenberger Feb 25, 2018

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.

Copy link
Member Author

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,
Copy link
Contributor

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 Polly TimeoutPolicy on 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.

Copy link
Member Author

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);

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?

Copy link
Member Author

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.
@rynowak rynowak force-pushed the rynowak/polly-registry branch from 33ce079 to 30a6862 Compare February 28, 2018 07:46
@rynowak
Copy link
Member Author

rynowak commented Feb 28, 2018

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");
Copy link
Member Author

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 😆

@rynowak rynowak changed the title Design proposal for configuration for registry Add support for policy registry Feb 28, 2018
@rynowak
Copy link
Member Author

rynowak commented Feb 28, 2018

Updated and added tests. I converted the existing builder tests into end-to-ends.

@rynowak rynowak force-pushed the rynowak/polly-registry branch from 2804537 to b268058 Compare February 28, 2018 20:29
@rynowak rynowak merged commit df30496 into dev Mar 3, 2018
@rynowak rynowak deleted the rynowak/polly-registry branch March 3, 2018 06:13
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants