Skip to content
reisenberger edited this page Oct 15, 2016 · 1 revision

PolicyWrap

Purpose

To provide a simple way to combine resilience strategies.

Concept

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

Syntax and examples

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 strongly-typed

When you wrap non-generic (non-strongly-typed) Policys 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).

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

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.