Skip to content
reisenberger edited this page Jul 4, 2017 · 29 revisions

PolicyWrap (v5.0 onwards)

Purpose

To provide a simple way to combine resilience strategies.

Concept

PolicyWrap provides a flexible way to encapsulate applying multiple resilience policies to delegates in a nested fashion (sometimes known as the 'Russian-Doll' or 'onion-skin-layers' model).

Rather than the longhand:

fallback.Execute(() => breaker.Execute(() => retry.Execute(action)));

A PolicyWrap can express 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 multiple TResult 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 one PolicyWrap. (Each PolicyWrap can weave a different thread through available policy instances; policy instances do not have only one possible antecedent or subsequent, in wraps.)

From a functional-programming or mathematical perspective, this is functional composition of a higher-order function, 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).

Syntax and examples

Syntax examples given are sync; comparable async overloads exist for asynchronous operation. See readme and wiki for more details.

Static syntax

PolicyWrap policyWrap = Policy.Wrap(fallback, cache, retry, breaker, timeout, bulkhead);

Instance syntax

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

Building wraps flexibly from components

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 */ });    

Non-generic versus generic

When you wrap non-generic Policys together, the PolicyWrap remains non-generic, no matter how many policies in the wrap. Any .Execute<TResult> can be executed through the non-generic wrap using the .Execute<TResult> generic method.

When you include a generic-typed SomePolicy<TResult> in a wrap, the PolicyWrap as a whole becomes generic-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).

Operation

  • 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.

Usage recommendations

Ordering the available policy-types in a wrap

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.

Using the same type of policy more than once in a wrap

You may use the same policy-type multiple times (eg two RetryPolicys; two FallbackPolicys) in the same wrap, to set different handling strategies for different exceptions/faults as part of one overall resilience strategy. For example:

  • You might 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.
  • You might nest multiple cache policies with different kinds of cache: eg memory-cache, backed up by disk or cloud cache.

ExecuteAndCapture

We do not recommend the use of .ExecuteAndCapture(...) with PolicyWrap. A PolicyWrap instance may contain multiple policies each specifying .Handle<>(), .HandleResult<>(), .Or<>() and .OrResult<>() clauses; and these clauses may differ for different policies in the wrap. .ExecuteAndCapture(...) on a PolicyWrap instance cannot therefore (at v5.0), in a simple and clear way, identify whether a given result should be considered FaultType.ResultHandledByThisPolicy or FaultType.ExceptionHandledByThisPolicy.

Interacting with policy operation

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.

Thread safety and policy reuse

Thread safety

PolicyWrap is thread-safe: multiple calls may safely be placed concurrently through a policy instance.

Policy reuse

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.

Clone this wiki locally