User Guide

Kampute.Resilience retries operations after failures that may be temporary. You choose which failures to retry, how long to wait between attempts, and when to stop.

Start with a retry policy. Add result handling or callbacks when you need them, and use a manual loop when your workflow needs more control.

Getting Started

The examples assume a C# console project with Kampute.Resilience referenced. LoadAsync represents your application's asynchronous operation: it accepts a CancellationToken, returns a Task<string>, and may throw TimeoutException. Replace it with your own operation.

A strategy decides how long to wait and when to stop. RetryOn turns it into a retry policy that also decides which failures to retry, and ExecuteAsync runs the operation under that policy:

using System;
using System.Threading;
using System.Threading.Tasks;
using Kampute.Resilience;

var retry = RetryStrategies
    .Constant(TimeSpan.FromSeconds(1))
    .WithMaxRetries(3)
    .RetryOn<TimeoutException>();

var value = await retry.ExecuteAsync(ct => LoadAsync(ct));

This policy waits one second between attempts and allows three retries after the initial attempt: at most four attempts. A TimeoutException requests a retry; other exceptions reach the caller immediately. If the retry budget is exhausted, awaiting ExecuteAsync throws the last exception. On success, it returns the operation's value.

The later examples reuse this retry policy and the same LoadAsync operation.

Choose failures that your application expects to clear, and operations that are safe to repeat. Retrying can repeat work that an earlier attempt already performed.

Retry Strategies

A strategy, represented by IRetryStrategy, defines the waiting pattern and retry limits. A session, represented by IRetrySession, tracks the progress of one operation in a retry loop that you write yourself. Policies keep the progress of each execution themselves.

Built-in strategies, and the policies built from them, can be shared across operations: each execution has its own retry budget.

Built-in Strategies

RetryStrategies provides these waiting patterns. Increasing the delay as failures continue is called backoff.

StrategyWaiting patternTypical use
NoneNo retries.Report the first failure to the caller.
OnceOne retry after the specified delay.Give a brief interruption one chance to clear.
ConstantThe same delay before each retry.Check for recovery at a steady interval.
LinearAdd a fixed amount to each subsequent delay.Reduce retry frequency gradually.
FibonacciGrow delays using the Fibonacci sequence.Increase waits more moderately than doubling them.
ExponentialMultiply each subsequent delay; the default factor is two.Increase recovery time quickly after repeated failures.

Specify delays as a nonnegative TimeSpan or a number of milliseconds. Zero allows an immediate retry. Except for None and Once, these strategies allow retries without a count or elapsed-time limit; add the limits your operation needs.

Strategy Modifiers

Modifiers adjust a strategy. For example, this strategy doubles the delay after each failure but caps each wait at ten seconds:

var backoff = RetryStrategies
    .Exponential(TimeSpan.FromSeconds(1))
    .WithMaxDelay(TimeSpan.FromSeconds(10))
    .WithMaxRetries(3);
ModifierUse it to
WithMaxRetriesLimit retries after the initial attempt.
WithMaxDelayCap each wait without limiting the retry count.
WithMaxElapsedTimeStop scheduling retries after a time budget, shortening a wait that would extend beyond it.
WithJitterRandomly vary delays so operations that fail together can retry at different times.

Order matters when modifiers change delays. Apply jitter before the delay cap to keep waits within that cap; jitter applied after the cap can lengthen a capped wait.

An elapsed-time limit does not interrupt an attempt already running. Use caller cancellation when you also need to stop the operation.

Custom Strategies

Implement IRetryStrategy when you need a different waiting pattern. Its TryGetRetryDelay method receives the elapsed time and the number of retries already made, then returns whether another retry is allowed and how long to wait. Custom strategies can use the same modifiers and policies.

Retry Policies

A policy, represented by RetryPolicy, combines a strategy with the failures to retry and callbacks for each retry. Start one from a strategy with a RetryOn method, then run operations with ExecuteAsync or Execute. ExecuteAsync awaits both the operation and the waits between retries; Execute blocks the calling thread during the waits.

Policies are immutable: each method returns a new policy and leaves the original unchanged. Build a policy once, store it, and share it between concurrent operations.

Choosing Exceptions

RetryOn<TException> retries the exceptions of a type, optionally only those that satisfy a predicate, and RetryOn with a Func<Exception, bool> accepts any condition. Each call adds to the exceptions already selected:

var transient = RetryStrategies
    .Constant(TimeSpan.FromSeconds(1))
    .WithMaxRetries(3)
    .RetryOn<TimeoutException>()
    .RetryOn<IOException>(error => error is not FileNotFoundException);

A policy without a RetryOn call retries every exception except caller cancellation, and so do the ExecuteAsync and Execute methods that you call on a strategy directly. Select exceptions when only particular failures are recoverable.

Result Retries

RetryOnResult retries a returned value that another attempt might improve. It returns a RetryPolicy<T> for operations returning that type, which keeps the exceptions, delay override, and retry handlers already configured. In this example, an empty string requests a retry:

var policy = retry.RetryOnResult<string>(value => string.IsNullOrEmpty(value));

var value = await policy.ExecuteAsync(ct => LoadAsync(ct));

TimeoutException is still retried, while the result predicate handles returned values. When result retries are exhausted, execution returns the last result. Here, the caller must handle an empty string if the operation never produces a useful value.

Retry Notifications

OnRetry observes approved retries. The handler runs before the wait and receives a RetryContext<T> that describes the failed attempt; on a policy that retries only exceptions, it receives a RetryContext. This policy logs each retry of the result-retry example:

var logged = policy.OnRetry(context =>
    Console.WriteLine($"Attempt {context.AttemptNumber}: retry in {context.Delay}."));

AttemptNumber starts at one, so a notification for attempt one means the initial attempt needs a retry. Delay is the wait before the next attempt. Each OnRetry call adds a handler, and handlers run in the order they were added. They do not run when no retry follows.

Result Cleanup

If results own resources, such as HTTP responses or streams, use OnDiscarded to release the results that execution does not return, or DisposeDiscarded to dispose them:

var send = RetryStrategies
    .Exponential(TimeSpan.FromSeconds(1))
    .WithMaxRetries(3)
    .RetryOnResult<HttpResponseMessage>(response => (int)response.StatusCode >= 500)
    .DisposeDiscarded();

Execution does not dispose results otherwise. Accepted results and the last result returned on exhaustion remain the caller's responsibility. The string example needs no cleanup.

Keep callbacks short. Result predicates, delay overrides, retry handlers, and discard handlers stop execution if they throw; the RetryPolicy<T> reference describes their exception behavior.

Delay Override

Use OverrideDelay when a result or exception should change the next wait. Its function receives the failed attempt, whose Delay is the delay that the strategy proposes, and returns the delay to wait. This override waits five seconds after an empty result and keeps the strategy's delay for exceptions:

var slower = policy.OverrideDelay(context =>
    context.HasResult ? TimeSpan.FromSeconds(5) : context.Delay);

HasResult distinguishes a returned value from an exception. The override runs only after the strategy allows a retry, and a later OverrideDelay call replaces an earlier one. The override also suits a retry time that the failure itself suggests, such as one carried by an exception.

An overriding delay replaces the proposed delay after modifiers have run, so it can exceed a delay cap or the wait remaining under an elapsed-time limit. Apply any required limits in the override or through caller cancellation.

Caller Cancellation

Pass the caller's token to ExecuteAsync or Execute and pass the operation's ct parameter to its asynchronous APIs. The token cancels retry waits; the operation must also observe it to stop its own work.

This example gives the operation and its retries a 30-second cancellation deadline:

using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));

var value = await retry.ExecuteAsync(ct => LoadAsync(ct), cancellation.Token);

An OperationCanceledException from the operation is not retried when that token is canceled. In an application, use the token supplied by the calling request or background job, or link it with a deadline token.

Passing State

Each Execute and ExecuteAsync method has an overload that passes a state value to every attempt. A lambda that uses local variables captures them, which allocates a closure on each call. Passing those values as state to a static lambda avoids that allocation, which can matter on frequently called paths. Use a tuple to pass several values:

var page = await retry.ExecuteAsync(
    (client, uri),
    static (state, ct) => state.client.GetStringAsync(state.uri, ct));

Here, client is an HttpClient and uri is the address to read.

Retry Sessions

Use a session directly when you need to control the workflow between attempts or supply your own scheduling.

Manual Retry Loops

StartSession creates a RetrySession for one operation. WaitToRetryAsync returns true after an approved wait, or false when the strategy allows no further retry.

This loop retries timeouts from LoadAsync with the strategy of the retry policy. It assumes cancellationToken is supplied by the caller:

var session = retry.Strategy.StartSession();

while (true)
{
    try
    {
        await LoadAsync(cancellationToken);
        break;
    }
    catch (TimeoutException)
    {
        if (!await session.WaitToRetryAsync(cancellationToken))
            throw; // Preserve the last failure when no retry remains.
    }
}

Create the session before the loop. Creating it inside the loop would restart the retry budget on every failure. Start a new session for each independent operation, and do not share a session between concurrent operations.

Custom Sessions

Implement IRetrySession when your application owns the retry decisions and waits of a loop that you write. To change only the decision or the delay, such as to wait for a retry time that the failure suggests, derive from RetrySession and override TryGetRetryDelay; the session still counts the retries, applies its delay limit, and waits. Policies do not use sessions; to base a policy's delay on the failure, use OverrideDelay.