Skip to content
martincostello edited this page Sep 28, 2023 · 29 revisions

PolicyWrap (v5.0 onwards)

ℹ️ This documentation describes the previous Polly v7 API. If you are using the new v8 API, please refer to pollydocs.org.

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(() => waitAndRetry.Execute(() => breaker.Execute(action)));

A PolicyWrap can express this:

fallback.Wrap(waitAndRetry).Wrap(breaker).Execute(action);

or equivalently:

fallback.Wrap(waitAndRetry.Wrap(breaker)).Execute(action);

or equivalently:

Policy.Wrap(fallback, waitAndRetry, breaker).Execute(action);

All PolicyWrap instances must contain two or more policies.

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.

For example, for Policy.Wrap(fallback, waitAndRetry, breaker):

Execution flow through a PolicyWrap comprising fallback, waitAndRetry, and breaker

(click to enlarge)

  • fallback is the outermost policy; it places its call through waitAndRetry;
    • waitAndRetry places its call through breaker;
      • breaker is the innermost policy, and (unless the circuit is open) executes the passed delegate.
        • the passed delegate succeeds; or returns a handled result; or throws.
      • breaker will update circuit stats for that outcome, then pass it back to ...
    • waitAndRetry, which will (for a fault) wait-and-retry, or fail when retries are exhausted; (for a success) return it; and pass back to ...
  • fallback, which will (for a fault) substitute the fallback result; (for a success) return it.

Syntax and examples

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

Instance-method syntax

var policyWrap = fallback.Wrap(cache).Wrap(retry).Wrap(breaker).Wrap(bulkhead).Wrap(timeout);
// or (functionally equivalent)
var policyWrap = fallback.Wrap(cache.Wrap(retry.Wrap(breaker.Wrap(bulkhead.Wrap(timeout)))));

The .Wrap() instance-method syntax is very flexible: the syntax can mix generic and non-generic policies. The compiler can infer generic type parameters for a PolicyWrap<TResult> where necessary.

Static-method syntax

Static syntax for non-generic policy wrap

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

This syntax only wraps non-generic policies together.

Static syntax for generic policy wrap

PolicyWrap<TResult> genericPolicyWrap = Policy.Wrap<TResult>(fallback, cache, retry, breaker, bulkhead, timeout);

This syntax only wraps generic policies together.

PolicyWrap characteristics

PolicyWrap is a Policy

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.

Functional composition

From a functional-programming or mathematical perspective, a PolicyWrap 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).

Flexible re-combination

Some interesting characteristics flow from the functional-composition and thread-safe aspects of PolicyWrap.

  • The same Policy instance can safely be used in more than one PolicyWrap in different ways. (Each PolicyWrap can weave a different path through the policy instances configured in your app. Policy instances need not have only one possible antecedent or subsequent, in the wraps in your app.)
  • A PolicyWrap can be onward-wrapped into further wraps, to build more powerful combinations.

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

Non-generic if made of non-generic policies

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.

Generic if any policy in the wrap is generic

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.

You can however mix non-generic and generic policies in the same PolicyWrap<TResult>, provided all the generic policies are for the same TResult type and provided the instance configuration syntax is used.

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-between-tries or waits-for-bulkhead-slot
RetryPolicy and CircuitBreaker Either retry wraps breaker, or vice versa. Judgment call. With longer delays between retries, we suggest WaitAndRetry wraps CircuitBreaker (the circuit-state might reasonably change in the delay between tries). With no/short delays between retries, we suggest CircuitBreaker wraps 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 might alternatively put a TimeoutPolicy outside the bulkhead, if you are allowing calls to queue for the bulkhead, and want to time-out how long they queue for.

The typical ordering of policies outlined above gives rise to an execution flow similar to the below:

(click to enlarge)

Execution flow through a PolicyWrap using all policies in a typical order

While the above presents one typical ordering and a fully-fledged strategy, PolicyWrap is intentionally fully flexible, allowing you to include only the resilience components you need, or to devise more complex strategies, as required.

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 is particularly useful for setting different handling strategies for different faults, as part of the overall strategy for a given call site.

  • You might retry more times or with shorter delay for one kind of exception, than another.
  • You might have one retry policy for typical transient faults; and a separate retry policy for a special case such as reauthentication; or honouring 429 RetryAfter headers.
  • 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.

Other policy types can also be used more than once in a wrap:

  • You can apply an overall timeout to the whole attempt (including waits-between-tries); as well as a separate timeout for each individual try.
  • You can nest multiple cache policies with different kinds of cache: eg memory-cache backed up by a cloud cache (if not using a cache provider which provides this double-cache approach intrinsically).

PolicyWrap with ExecuteAndCapture()

.ExecuteAndCapture(...) on a PolicyWrap captures whether the final outcome of the execution is one considered a fault by the outermost policy in the wrap.

Properties

PolicyKey

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.

Outer

Returns the outer policy of the pair represented by this PolicyWrap instance.

Inner

Returns the inner policy of the pair represented by this PolicyWrap instance.

Methods

All the below GetPolicies() methods are available from Polly v5.6+.

IEnumerable<IsPolicy> GetPolicies() returns all the policies configured in this PolicyWrap instance, in outer->inner order.

IEnumerable<TPolicy> GetPolicies<TPolicy>() returns all policies of type TPolicy configured in this PolicyWrap instance, in outer->inner order.

IEnumerable<TPolicy> GetPolicies<TPolicy>(Func<TPolicy, bool>) returns all policies of type TPolicy in this PolicyWrap instance matching the filter, in outer->inner order.

TPolicy GetPolicy<TPolicy>() returns a single policy of type TPolicy configured in this PolicyWrap instance; or null if none match.

TPolicy GetPolicy<TPolicy>(Func<TPolicy, bool>) returns a single of type TPolicy in this PolicyWrap instance matching the filter; or null if none match.

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 OperationKey to distinguish different call-site usages within logging and metrics.

Clone this wiki locally