-
Notifications
You must be signed in to change notification settings - Fork 2
(draft) PolicyWrap
To provide a simple way to combine resilience strategies.
PolicyWrap
provides a flexible way to encapsulate applying two or more policies to delegates in a nested fashion (sometimes known as the 'Russian-Doll' or 'onion-skin-layers' model).
Consider:
fallback.Execute(() => breaker.Execute(() => retry.Execute(action)));
A PolicyWrap
expresses this:
Policy.Wrap(fallback, breaker, retry).Execute(action);
or equivalently:
fallback.Wrap(breaker.Wrap(retry)).Execute(action);
or equivalently:
fallback.Wrap(breaker).Wrap(retry).Execute(action);
In these examples, retry
is the innermost policy, immediately wrapping the delegate; it will pass its results (or failure) back to breaker
; and in turn back to the outermost, fallback
.
A PolicyWrap
is just another Policy
, and has the same qualities:
- it is thread-safe
- it can be reused across multiple call sites
- a non-generic
PolicyWrap
can be used with the generic.Execute/Async<TResult>(...)
methods, across multipleTResult
types.
Also:
- a
PolicyWrap
can be onward-wrapped into further wraps, to build more powerful combinations. - the same
Policy
instance can thread-safely be used in more than onePolicyWrap
. (EachPolicyWrap
can weave a different thread through available policies; policies do not have only one possible antecedent or subsequent, in wraps.)
From a functional-programming or mathematical perspective, this is functional composition, and the resulting PolicyWrap
is a monad (as in Linq or Rx).
If applying a policy to a delegate is f(x)
(where f
is the Polly policy and x
the delegate), a PolicyWrap
allows you to express a(b(c(d(e(f(x))))))
(or: a(b(c(d(e(f)))))(x)
), (where a
to f
are policies, and x
the delegate).
PolicyWrap policyWrap = Policy.Wrap(fallback, cache, retry, breaker, timeout, bulkhead);
PolicyWrap policyWrap = fallback.Wrap(cache).Wrap(retry).Wrap(breaker).Wrap(timeout).Wrap(bulkhead);
// or (functionally equivalent)
PolicyWrap policyWrap = fallback.Wrap(cache.Wrap(retry.Wrap(breaker.Wrap(timeout.Wrap(bulkhead)))));
The instance syntax allows you to build variations on a theme, mixing common and site-specific resilience needs:
PolicyWrap commonResilience = Policy.Wrap(retry, breaker, timeout);
// ... then wrap in extra policies specific to a call site:
Avatar avatar = Policy
.Handle<Whatever>()
.Fallback<Avatar>(Avatar.Blank)
.Wrap(commonResilience)
.Execute(() => { /* get avatar */ });
// Share the same commonResilience, but wrap with a different fallback at another call site:
Reputation reps = Policy
.Handle<Whatever>()
.Fallback<Reputation>(Reputation.NotAvailable)
.Wrap(commonResilience)
.Execute(() => { /* get reputation */ });
When you wrap non-generic (non-strongly-typed) Policy
s together, the PolicyWrap
remains non-strongly-typed, no matter how many policies in the wrap. Any .Execute<TResult>
can be executed through the non-generic wrap.
When you include a strongly-typed SomePolicy<TResult>
in a wrap, the PolicyWrap
as a whole becomes strongly-typed PolicyWrap<TResult>
. This provides type-safety: it would be non-sensical to have (paraphrasing syntax)
fallback<int>.Execute(() => breaker<string>.Execute(() => retry<Foo>.Execute<Bar>(func)));
(just as Linq and Rx do not let you do this).
-
PolicyWrap
executes the supplied delegate through the layers or wrap: the outermost (leftmost in reading order) 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.
Policies can be combined flexibly in any order. It is worth however considering the following points:
Policy type | Common positions in a PolicyWrap
|
Explanation |
---|---|---|
FallbackPolicy |
Usually outermost | Provides a substitute value after all other resilience strategies have failed. |
FallbackPolicy |
Can also be used mid-wrap ... | ... eg as a failover strategy calling multiple possible endpoints (try first; if not, try next). |
CachePolicy |
As outer as possible but not outside stub fallbacks | As outer as possible: if you hold a cached value, you don't want to bother trying the bulkhead or circuit-breaker etc. But cache should not wrap any FallbackPolicy providing a placeholder-on-failure (you likely don't want to cache and serve the placeholder to all subsequent callers) |
TimeoutPolicy |
Outside any RetryPolicy , CircuitBreaker or BulkheadPolicy
|
... to apply an overall timeout to executions, including any delays-before-retry / wait for bulkhead |
RetryPolicy and CircuitBreaker
|
Either retry wraps breaker, or vice versa. | Judgment call. With longer delays between retries (eg async background jobs), we have Retry wrap CircuitBreaker (the circuit-state might reasonably change in the delay between tries). With no/short delays between retries, we have CircuitBreaker wrap Retry (don't take hammering the underlying system with three closely-spaced tries as cause to break the circuit). |
BulkheadPolicy |
Usually innermost unless wraps a final TimeoutPolicy ; certainly inside any WaitAndRetry
|
Bulkhead intentionally limits parallelization. You want that parallelization devoted to running the delegate, not eg occupied by waits for a retry. |
TimeoutPolicy |
Inside any RetryPolicy , CircuitBreaker or BulkheadPolicy , closest to the delegate. |
... to apply a timeout to an individual try. |
You may use the same policy-type multiple times (eg two RetryPolicy
s; two FallbackPolicy
s) in the same wrap. This allows you to define different strategies for different exceptions, in one overall resilience strategy. For example:
- You can retry more times or with shorter delay for one kind of exception, than another.
- You may want one kind of exception to immediately break the circuit; others to break more cautiously.
- You can provide different fallback values/messages for different handled faults.
A PolicyKey
can be attached to a PolicyWrap
, as with any other policy:
PolicyWrap commonResilience = Policy
.Wrap(retry, breaker, timeout)
.WithPolicyKey("CommonServiceResilience");
The wrap's PolicyKey
is exposed on the execution Context
as the property:
context.PolicyWrapKey
In a multiply-nested wrap, the PolicyKey
attached to the outermost wrap carries all through the execution as the context.PolicyWrapKey
.
The future Polly roadmap envisages adding metrics to Polly, with metrics for the overall execution time (latency) across a PolicyWrap
or elements of a wrap.
PolicyWrap
is thread-safe: multiple calls may safely be placed concurrently through a policy instance.
PolicyWrap
instances may be re-used across multiple call sites.
When reusing policies, use an ExecutionKey
to distinguish different call-site usages within logging and metrics.