FEATURE: Policy Wrap ( / pipeline)
See original GitHub issueProposal: 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 returnvoid
. - 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 carriesKey
information unifying the call: see Keys proposal #139 for details. - Care needed around implementations for
ExecuteAndCapture()
. Probably only the outermost policy in the wrap shouldExecuteAndCapture()
; others shouldExecute()
-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)
Issue Analytics
- State:
- Created 7 years ago
- Comments:6 (3 by maintainers)
Top GitHub Comments
@juancarrey , re:
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 supplyingfallback, 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 extratry / 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 likeonBreak
andonRetry
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 anIEnumerable<Exception>
orIEnumerable<DelegateResult<TResult>>
onPolicyResult
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?
An initial implementation of
PolicyWrap
can now be seen in the v5.0-alpha branch.