Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Policy Wrap ( / pipeline) #140

Closed
reisenberger opened this issue Jul 11, 2016 · 6 comments
Closed

FEATURE: Policy Wrap ( / pipeline) #140

reisenberger opened this issue Jul 11, 2016 · 6 comments

Comments

@reisenberger
Copy link
Member

reisenberger commented Jul 11, 2016

Proposal: Policy Wrap (was: Pipeline)

Purpose

To provide the ability to chain a series of policies together.

Scope

  • A meta-policy.
  • All sync and async variants
  • When result-returning policies placed in a wrap, all policies must return the same TResult
  • Or void-returning (Action-executing) policies may be wrapped; all polices in the wrap must return void.
  • Individual policies should be able to be used in more than one wrap.

Original proposal: Policy Pipeline

The roadmap originally proposed the following syntax:

// Possible syntax
Policy
  .Pipeline(params IEnumerable[] policies)
  .Execute(...) // Executes the action through the pipeline of policies, each policy wrapping the next.  

Some issues were:

  • The term pipeline very much connotes a one-way flow. In exception-handling this precisely isn't the case: exceptions thrown would then be passed 'backwards back through the pipeline' (? - ugly broken metaphor).
  • The syntax isn't conducive to flexibly composing pipelines by functional composition.

Revised proposal: Policy Wrap

The revision proposes the name Policy Wrap:

  • The term wrap more clearly connotes outer and inner layers, and the idea of wrapping a function with another (what is actually happenning).
  • It's the classic 'onion-layers' metaphor, with outer policies executing next-inner policies, executing next-inner, until getting to execute the user delegate (at the 'core' of the onion).
  • Easier to grok what happens with exceptions: they get thrown back outwards through the layers.
  • The term wrap plays better with a flexibly-composed, functionally-composed policy - see below.

Syntax proposal

Both static and instance configuration methods are now proposed:

// Static configuration syntax as before:
PolicyWrap policyWrap = Policy.Wrap(fallback, cache, retry, circuitBreaker, timeout, throttle); 
policyWrap.Execute(myAction);

// Instance syntax
Policy retry = ...       // some retry policy
Policy breaker = ...     // some breaker
Policy timeout = ...     // some timeout

retry.Wrap(breaker).Execute(myAction); // Inline wrap usage showing simple functional composition

retry.Wrap(breaker).Wrap(throttle).Execute(myAction); // Inline wrap usage chaining three policies
retry.Wrap(breaker.Wrap(throttle)).Execute(myAction); // (functionally equivalent to preceding line)

// Possible use of instance syntax configuring a policy for later use
PolicyWrap myWrap = fallback.Wrap(cache).Wrap(retry).Wrap(breaker).Wrap(timeout).Wrap(throttle);
PolicyWrap myWrap = fallback.Wrap(cache.Wrap(retry.Wrap(breaker.Wrap(timeout.Wrap(throttle))))); // (functionally equivalent to preceding)


Value of functional composition

With the original 'one-shot' static syntax, it was not intuitive how to create a pipeline that was a slight variation on another. It could lead to questions about 'how to [consistently] insert something into the middle of an IEnumerable/ICollection'. Mutable pipelines also potentially have issues in multi-threaded, concurrent scenarios.

The instance syntax of .Wrap(...) allows more flexible functional-composition of a PolicyWrap:

Policy myWrap = ...            // some existing wrap or policy - maybe even passed in to this code from elsewhere
bool useCache = ...            // some value, maybe passed in
CachePolicy cachePolicy = ...  // from somewhere
myWrap = useCache ? cachePolicy.Wrap(myWrap) : myWrap;
// etc

The functional-composition paradigm matches LINQ and Rx: successively composing functional transformations on the function-so-far, before actual execution.

Operation

  • Executes the supplied user delegate through the layers or wrap: the outermost policy executes the next inner, which executes the next inner, etc, until the innermost policy executes the user delegate.
  • Exceptions bubble back outwards (until handled) through the layers.
  • The Context instance carries Key information unifying the call: see Keys proposal FEATURE: Add KEYs to policies and executions #139 for details.
  • Care needed around implementations for ExecuteAndCapture(). Probably only the outermost policy in the wrap should ExecuteAndCapture(); others should Execute() -and-throw.

Naming

Any alternative naming suggestions?

Comments?

Comments? Alternative suggestions? Extra considerations to bear in mind?

Have scenarios to share where you'd use this? (could inform development)

@juancarrey
Copy link

juancarrey commented Jul 12, 2016

I had a PolicyChain which actually implemented this Wrap very similarly, except that ExecuteAndCapture actually was done by the innermost policy, and returned backwards. (I am not sure which solution would be ideal - option at builder ?)
The chain implemented IPolicy, and any IPolicy could be chained afterwards (wrapped). The innermost PolicyChain had no next policy, so it just delegates to the IPolicy inside. (A policy-chain contained 1 or 2 policies, the current and the next one if any.)

The IPolicy was also implemented by a CombinedPolicy which "merges" an async policy with a sync policy, removing the runtime exceptions of using an async policy synchronouslly and a sync policy asynchronouslly.
I would say this "combine" would be a great addition to Polly if you do this transparently:
when calling .WaitAndRetry you actually create 2 policies and combine them: .WaitAndRetry + .WaitAndRetryAsync with the same parameters (so it forces the policies to be equally deffined). Same for the rest of policy creations.

IPolicy extended from IAsyncPolicy and from ISyncPolicy, which made policies more clearly separated and avoid any miss-usage of them.

@reisenberger
Copy link
Member Author

reisenberger commented Jul 12, 2016

@juancarrey That's great to hear: thanks for sharing!

The chain implemented IPolicy, and any IPolicy could be chained afterwards (wrapped).
The innermost PolicyChain had no next policy, so it just delegates to the IPolicy inside.
(A policy-chain contained 1 or 2 policies, the current and the next one if any.)

@juancarrey That's exactly the implementation I am planning. Each policy in the Wrap only needs to know about the next, and this actually simplifies the wrap implementation greatly. Except that we have consciously held off IPolicy while in this phase of deep product growth (discussed in #90).

@reisenberger
Copy link
Member Author

reisenberger commented Jul 12, 2016

@juancarrey , re:

ExecuteAndCapture actually was done by the innermost policy, and returned backwards.
[should it be] option at builder ?

My thought so far was that the capture of ExecuteAndCapture() doesn't want to be done by any inner policy: if an inner execution faults, you want that fault thrown back on to the next-outer policy (not neutralised-by-Capture), as the next outer policy might deal with it. For example, in the sample wrap in the proposal (fallback, cache, retry, circuitBreaker, timeout, throttle), if the throttle or timeout rejects execution, you may well want the circuitbreaker to register that (configurable by which exceptions the breaker handles), you may want to retry after a delay (ditto), and you very likely want the fallback to detect that failure and provide the fallback substitute. That was the (not accidental) logic in supplying fallback, cache, retry, circuitBreaker, timeout, throttle as the example.

My assumption so far about the ExecuteAndCapture() feature has been that its purpose is (after the policy or wrap exhausts all other options...) to capture any final unhandled fault into an 'outcome' class, rather than have the caller have to place an extra try / catch round the execution (hence, again, only applying to the outermost call). Is this sounding sensible to other people?? (Keen to hear if I missing something about how ExecuteAndCapture() might operate at inner levels!...)


If the question is about how to capture 'what went on [what exception was maybe thrown] at the inner levels', then my thinking so far is that the complexity dictates we need to look to another mechanism: either onSomething delegates like onBreak and onRetry in the current policies (great for logging); and/or capturing through observing events emitted from all policies in the wrap, as first mooted here. Something like an IEnumerable<Exception> or IEnumerable<DelegateResult<TResult>> on PolicyResult wouldn't (could it?) capture enough detail about on which inner policy the exception occurred on (and might contain lots of repeats if the same exception was thrown outwards through several levels?).

Thoughts, anyone?

@juancarrey
Copy link

@reisenberger That makes sense to be the outermost

@reisenberger reisenberger self-assigned this Jul 17, 2016
@drewburlingame
Copy link

i like the idea of emitting events. your examples of using the events for telemetry and logging were great examples of some of the benefits.

@reisenberger
Copy link
Member Author

An initial implementation of PolicyWrap can now be seen in the v5.0-alpha branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants