Skip to content

Polly concept and architecture

reisenberger edited this page Nov 6, 2017 · 9 revisions

This post provides a brief overview of the internal concept and architecture of Polly as at v5.5.0.

The Polly concept

The external focus of Polly is resilience. From the perspective of its internal concept, however, the essence of Polly is:

Polly transforms how an action is actioned.

That concept was expressed, right from the outset (commit 1), in the definition of the implementation of a Policy:

Action<Action> _exceptionPolicy

Action<Action>: The inner Action is the delegate you pass to Polly to execute; the outer Action is how the policy will inflect or transform execution of that.

Polly now supports async, cancellation, an execution context, and handling returned results, so the fullest version of a policy implementation is somewhat elaborated:

Func<Func<Context, CancellationToken, Task<TResult>>, Context, CancellationToken, bool, Task<TResult>> _asyncExecutionPolicy;

but the essential concept remains.

The Polly architecture

Implementation of a specific policy type

Each policy type in Polly is implemented by a group of related classes covering different aspects of the policy implementation.

File name Aspect
FooPolicyEngine.cs (example) The implementation of the policy (as described in the concept section above)
FooPolicySyntax.cs (example) Syntax overloads for configuring the policy
FooPolicy.cs (example) Class representing a configured instance of that policy
IFooPolicy.cs (example) Interface identifying the policy type, and gathering any properties/methods specific to it

The files are found in a folder named after the Policy (example).

Individual policy types may have further supporting classes: retry has implementations of IRetryPolicyState managing the retries for each execution; circuit-breaker has implementations of ICircuitController managing circuit state and statistics.

All policies operate across two key dimensions

Polly supports both sync and async executions; and executing both void and TResult-returning delegates. This leads conceptually to four forms (2 x 2-dimensions) for each policy:

policy forms sync async
void-returning sync void-returning async Task-returning
TResult-returning sync TResult-returning async Task<TResult>-returning

The four forms can be found running through most aspects of a policy described above (engine; syntax; policy; interface).

To avoid code duplication in the most critical part of the codebase - the implementation of policy behaviour - only TResult-returning implementations exist. Executions returning void are fulfilled via the TResult-returning implementation (example), substituting a flyweight empty struct.

Abstract base classes common to all policy types

Abstract base classes Policy<TResult> and Policy provide functionality common to all policy types, such as the full range of execution overloads:

  • Execute(...)
  • ExecuteAndCapture(...)
  • ExecuteAsync(...)
  • ExecuteAndCaptureAsync(...)

Other functionality provided by the base classes includes managing execution content, and policy keys.

Generic and non-generic, separate class hierarchies

The generic, strongly-typed form Policy<TResult> provides compile-time type-binding for rich operations involving TResult. The non-generic form Policy offers flexibility for simpler operations. Policy<TResult> necessarily does not extend Policy.

Interfaces

Policy-type interfaces identify specific policy types, and any properties and methods they expose.

Class, base-class and interface relationships

The relationship between policy-specific classes, the abstract base classes, and policy-type interfaces is then as follows:

policytypepolicybaseclassandinterfacerelationships

Interacting with policy operation

Delegate hooks

Most all policies expose delegate hooks, allowing you to hook in extra behaviour when key events occur within the lifecycle of a call through the policy. For example:

  • onRetry: invoked before a new retry, by retry policies
  • onBreak: invoked when the circuit breaks, by circuit-breaker policies

Properties

Stateful policy types often expose their state through properties: for example, circuitBreaker.State.

Methods

Some policies expose methods allowing you to affect the state of the policy: for example, circuitBreaker.Isolate().

Other supporting classes

Fluent-builder syntax

Reactive policies react to specific exceptions or return result values. The Handle syntax and Or syntax define which exceptions or results the policy will handle, and result in a PolicyBuilder instance. The syntax overloads for reactive policies such as retry are thus extension methods on PolicyBuilder. PolicyBuilder expresses the faults the policy handles in ExceptionPredicates and ResultPredicates.

Preemptive or proactive policies do not respond to specific faults, and thus their syntax overloads are directly on Policy.

Execution context

Context defines some common context which travels with each execution, and allows for user-definable context to be passed into executions.

Passing execution results to delegate hooks and ExecuteAndCapture

DelegateResult expresses the result of an individual execution of the passed delegate - for example when passed to a policy hook such as onRetry.

PolicyResult expresses the overall result of execution through a policy, as returned by an ExecuteAndCapture(...) overload.

Other points of interest

SystemClock

Polly abstracts the system clock. Unit tests which would otherwise incur time delays - for example, waiting the relevant duration until a broken circuit transitions from closed to half-open state - instead manipulate the abstracted clock, allowing these tests to run without the equivalent real-time delay.

TimedLock

The original Polly maintainers (prior to AppvNext) introduced the TimedLock class. Rather than deadlocking on a lock deadlock, this throws if the lock cannot be obtained within a given timeout, allowing easier debugging of deadlocks. The implementation uses the same underlying Monitor class as the language's lock statement, and thus is of comparable performance.

Code analysis - and the fact that no circuit-breaker locks have been reported in five years - has long suggested this class could be removed, but it offers some regression value. Future plans for circuit-breaker include moving to the use of Interlocked, which would also obviate the TimedLock class.

Unit tests

Polly's internal unit tests number ~1500 at Polly v5.5.0. They are grouped by policy type, and generally by the dimensions of implementation described earlier in this article.

Clone this wiki locally