Polly concept and architecture - App-vNext/Polly GitHub Wiki
âšī¸ This documentation describes the previous Polly v7 API. If you are using the new v8 API, please refer to pollydocs.org.
This post provides a brief overview of the internal concept and architecture of Polly as at v5.5.0.
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.
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.
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 Policy
, Policy<TResult>
, AsyncPolicy
and AsyncPolicy<TResult>
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.
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
.
Policy-type interfaces identify specific policy types, and any properties and methods they expose.
The relationship between policy-specific classes, the abstract base classes, and policy-type interfaces is then as follows:
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
Stateful policy types often expose their state through properties: for example, circuitBreaker.State
.
Some policies expose methods allowing you to affect the state of the policy: for example, circuitBreaker.Isolate()
.
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 ExceptionPredicate
s and ResultPredicate
s.
Preemptive or proactive policies do not respond to specific faults, and thus their syntax overloads are directly on Policy
.
Context
defines some common context which travels with each execution, and allows for user-definable context to be passed into executions.
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.
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 open to half-open state - instead manipulate the abstracted clock, allowing these tests to run without the equivalent real-time delay.
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 that no locking bugs are introduced.
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.