Skip to content

Retry Policy

Marco Breveglieri edited this page Nov 24, 2025 · 1 revision

Retry Policy API

Unit: Murphy.Policy.Retry

The Retry policy automatically retries failed operations with configurable retry counts and delays between attempts.

Overview

The Retry pattern is useful for handling transient failures - temporary problems that may resolve if you try again, such as:

  • Network timeouts
  • Temporary service unavailability
  • Database connection failures
  • Resource contention

Table of Contents


TRetryContext Class

Contains information about the current retry attempt and allows dynamic modification of retry behavior.

Declaration

type
  TRetryContext = class(TObject)
  private
    FAttempts: Integer;
    FException: Exception;
    FWaitDelay: TTimeSpan;
  public
    property Attempts: Integer read FAttempts;
    property Exception: Exception read FException;
    property WaitDelay: TTimeSpan read FWaitDelay write FWaitDelay;
  end;

Properties

Attempts

property Attempts: Integer read FAttempts;

Description: The number of attempts made so far (including the current one).

Type: Integer (read-only)

Values:

  • 1 - First attempt (initial execution)
  • 2 - First retry
  • 3 - Second retry
  • And so on...

Usage: Check attempt count in When predicate to implement custom retry logic.

Exception

property Exception: Exception read FException;

Description: The exception that caused the current retry attempt.

Type: Exception (read-only)

Usage: Access exception details in When predicate to make decisions based on the specific error.

WaitDelay

property WaitDelay: TTimeSpan read FWaitDelay write FWaitDelay;

Description: The delay that will be applied before the next retry attempt.

Type: TTimeSpan (read-write)

Usage: Modify this property in a When predicate to implement exponential backoff or other dynamic delay strategies.

Example:

Policy := TRetryBuilder
  .Handle(Exception)
  .Retry(5)
  .Wait(TTimeSpan.FromSeconds(1))
  .When(function(Context: TRetryContext): Boolean
        begin
          // Exponential backoff: double the delay each time
          Context.WaitDelay := TTimeSpan.FromMilliseconds(
            Context.WaitDelay.TotalMilliseconds * 2
          );
          Result := True;  // Continue retrying
        end)
  .Build;

IRetryPolicy Interface

The interface for the Retry policy pattern.

Declaration

type
  IRetryPolicy = interface(IPolicy)
    ['{7F6C8429-D08C-4ADF-82F6-FD96747CCA4C}']
    procedure Execute(AProc: TProc);
    function Retry(ATimes: Integer = 1): IRetryPolicy;
    function RetryForever: IRetryPolicy;
    function RetryOnce: IRetryPolicy;
    function Wait(ADelay: TTimeSpan): IRetryPolicy;
    function When(APredicate: TPredicate<TRetryContext>): IRetryPolicy;
  end;

Methods

Execute

procedure Execute(AProc: TProc);

Description: Executes the provided procedure with automatic retry on failure.

Parameters:

  • AProc: TProc - Anonymous procedure to execute

Behavior:

  1. Executes AProc
  2. If successful, returns immediately
  3. If exception is thrown and handled by policy:
    • Checks When predicate (if configured)
    • Waits for specified delay
    • Retries up to configured limit
  4. Re-raises exception if:
    • Exception type not handled by policy
    • When predicate returns False
    • Retry limit exceeded

Example:

RetryPolicy.Execute(
  procedure
  begin
    HTTP.Get('https://api.example.com/data');
  end);

Retry

function Retry(ATimes: Integer = 1): IRetryPolicy;

Description: Configures the number of retry attempts.

Parameters:

  • ATimes: Integer - Number of retries to attempt (default: 1)

Returns: IRetryPolicy - Self for method chaining

Total Attempts: ATimes + 1 (initial attempt plus retries)

Examples:

// Retry 3 times = 4 total attempts (1 initial + 3 retries)
Policy := TRetryBuilder.Handle(Exception).Retry(3).Build;

// Retry once = 2 total attempts (1 initial + 1 retry) - same as RetryOnce
Policy := TRetryBuilder.Handle(Exception).Retry(1).Build;

// No retries = 1 total attempt (initial only)
Policy := TRetryBuilder.Handle(Exception).Retry(0).Build;

Notes:

  • Default is 1 retry if not specified
  • ATimes can be 0 for no retries

RetryForever

function RetryForever: IRetryPolicy;

Description: Configures the policy to retry indefinitely until success.

Returns: IRetryPolicy - Self for method chaining

Example:

Policy := TRetryBuilder
  .Handle(Exception)
  .RetryForever
  .Wait(TTimeSpan.FromSeconds(5))
  .Build;

Warning: Use with caution! Ensure you have:

  • A When predicate to eventually stop retrying, or
  • External monitoring to detect infinite retry loops, or
  • Confidence that the operation will eventually succeed

Use Cases:

  • Critical operations that must succeed
  • Operations with guaranteed eventual consistency
  • Background workers that should never give up

RetryOnce

function RetryOnce: IRetryPolicy;

Description: Convenience method to configure exactly one retry (2 total attempts).

Returns: IRetryPolicy - Self for method chaining

Equivalent to: Retry(1)

Example:

// These are equivalent:
Policy1 := TRetryBuilder.Handle(Exception).RetryOnce.Build;
Policy2 := TRetryBuilder.Handle(Exception).Retry(1).Build;

Wait

function Wait(ADelay: TTimeSpan): IRetryPolicy;

Description: Configures the delay between retry attempts.

Parameters:

  • ADelay: TTimeSpan - Time to wait between retries

Returns: IRetryPolicy - Self for method chaining

Default: 1 second if not specified

Examples:

// Wait 2 seconds between retries
Policy := TRetryBuilder
  .Handle(Exception)
  .Retry(3)
  .Wait(TTimeSpan.FromSeconds(2))
  .Build;

// Wait 500 milliseconds
Policy := TRetryBuilder
  .Handle(Exception)
  .Retry(5)
  .Wait(TTimeSpan.FromMilliseconds(500))
  .Build;

// Wait 1 minute
Policy := TRetryBuilder
  .Handle(Exception)
  .Retry(3)
  .Wait(TTimeSpan.FromMinutes(1))
  .Build;

Notes:

  • Delay applies between attempts, not before the first attempt
  • In test mode (MurphyTestModeEnabled = True), delays are skipped
  • Can be modified dynamically in When predicate via Context.WaitDelay

When

function When(APredicate: TPredicate<TRetryContext>): IRetryPolicy;

Description: Configures a predicate to control retry behavior dynamically based on the current context.

Parameters:

  • APredicate: TPredicate<TRetryContext> - Function that receives context and returns whether to retry

Predicate Returns:

  • True - Continue with retry
  • False - Stop retrying and re-raise exception immediately

Returns: IRetryPolicy - Self for method chaining

Use Cases:

  • Exponential backoff
  • Conditional retry based on exception details
  • Maximum total retry time
  • Custom retry logic based on attempt count

Examples:

// Exponential backoff
Policy := TRetryBuilder
  .Handle(Exception)
  .Retry(5)
  .Wait(TTimeSpan.FromSeconds(1))
  .When(function(Context: TRetryContext): Boolean
        begin
          // Double the delay each retry
          Context.WaitDelay := TTimeSpan.FromMilliseconds(
            Context.WaitDelay.TotalMilliseconds * 2
          );
          Result := True;
        end)
  .Build;

// Retry only for specific HTTP status codes
Policy := TRetryBuilder
  .Handle(EIdHTTPProtocolException)
  .Retry(3)
  .When(function(Context: TRetryContext): Boolean
        begin
          if Context.Exception is EIdHTTPProtocolException then
          begin
            var StatusCode := EIdHTTPProtocolException(Context.Exception).ErrorCode;
            // Only retry on 503 (Service Unavailable) or 429 (Too Many Requests)
            Result := (StatusCode = 503) or (StatusCode = 429);
          end
          else
            Result := True;
        end)
  .Build;

// Stop after total retry time exceeds threshold
var
  StartTime: TDateTime;
begin
  StartTime := Now;

  Policy := TRetryBuilder
    .Handle(Exception)
    .RetryForever
    .Wait(TTimeSpan.FromSeconds(2))
    .When(function(Context: TRetryContext): Boolean
          begin
            // Stop retrying after 30 seconds total
            var ElapsedSeconds := SecondsBetween(Now, StartTime);
            Result := ElapsedSeconds < 30;
          end)
    .Build;
end;

TRetryPolicy Class

Concrete implementation of the Retry policy pattern.

Declaration

type
  TRetryPolicy = class(TPolicy, IRetryPolicy)
  private
    FRetryTimes: Integer;
    FWaitDelay: TTimeSpan;
    FWhenPredicate: TPredicate<TRetryContext>;
  public
    constructor Create(AExceptionTypes: TArray<ExceptClass>); override;
    procedure Execute(AProc: TProc);
    function Retry(ATimes: Integer = 1): IRetryPolicy;
    function RetryForever: IRetryPolicy;
    function RetryOnce: IRetryPolicy;
    function Wait(ADelay: TTimeSpan): IRetryPolicy;
    function When(APredicate: TPredicate<TRetryContext>): IRetryPolicy;
  end;

Constructor

constructor Create(AExceptionTypes: TArray<ExceptClass>); override;

Description: Creates a retry policy instance with default configuration.

Parameters:

  • AExceptionTypes: TArray<ExceptClass> - Exception types to handle

Defaults:

  • FRetryTimes: 1 (one retry = 2 total attempts)
  • FWaitDelay: 1 second
  • FWhenPredicate: nil (no predicate)

Note: Typically called by builder, not directly by users.

Implementation Details

The Execute method (Murphy.Policy.Retry.pas:88) implements the retry logic:

  1. Creates TRetryContext to track state
  2. Enters retry loop:
    • Increments attempt counter
    • Executes user code
    • On success: breaks loop and returns
    • On exception:
      • Checks if exception is handled
      • Checks When predicate (if configured)
      • Checks retry limit
      • Waits for delay
      • Continues to next iteration
  3. Cleans up context

Key Points:

  • FRetryTimes = -1 means retry forever
  • Exception is re-raised if not handled, predicate returns false, or limit exceeded
  • Delay is applied via Scheduler.WaitFor (respects test mode)

TRetryBuilder Class

Builder class for creating retry policies with fluent configuration.

Declaration

type
  TRetryBuilder = class sealed(TPolicyBuilder<IRetryPolicy>)
  public
    class function Handle(AExceptionTypes: TArray<ExceptClass>): IRetryPolicy; override;
  end;

Class Methods

Handle

class function Handle(AExceptionTypes: TArray<ExceptClass>): IRetryPolicy; override;

Description: Creates a new retry policy builder configured to handle specified exception types.

Parameters:

  • AExceptionTypes: TArray<ExceptClass> - Array of exception types to handle

Returns: IRetryPolicy - Policy instance ready for further configuration

Usage:

// Single exception
Policy := TRetryBuilder
  .Handle(EIdHTTPProtocolException)
  .Retry(3)
  .Build;

// Multiple exceptions
Policy := TRetryBuilder
  .Handle([
    EIdHTTPProtocolException,
    EIdSocketError
  ])
  .Retry(3)
  .Build;

Note: The policy instance returned is both the policy and the builder - you can chain configuration methods and then use Execute directly without calling Build.


Usage Examples

Example 1: Basic Retry

uses
  Murphy.Policy.Retry;

var
  Policy: IRetryPolicy;
begin
  Policy := TRetryBuilder
    .Handle(EIdHTTPProtocolException)
    .Retry(3)
    .Wait(TTimeSpan.FromSeconds(2))
    .Build;

  Policy.Execute(
    procedure
    begin
      WriteLn('Attempting HTTP request...');
      HTTP.Get('https://api.example.com/data');
      WriteLn('Success!');
    end);
end;

Example 2: Retry Forever with Predicate

var
  Policy: IRetryPolicy;
  AttemptCount: Integer = 0;
begin
  Policy := TRetryBuilder
    .Handle(Exception)
    .RetryForever
    .Wait(TTimeSpan.FromSeconds(5))
    .When(function(Context: TRetryContext): Boolean
          begin
            Inc(AttemptCount);
            WriteLn('Attempt ', AttemptCount, ' failed. Retrying...');

            // Stop after 10 attempts even though policy is RetryForever
            Result := AttemptCount < 10;
          end)
    .Build;

  Policy.Execute(
    procedure
    begin
      // Critical operation that must succeed
      PerformCriticalOperation;
    end);
end;

Example 3: Exponential Backoff

var
  Policy: IRetryPolicy;
begin
  Policy := TRetryBuilder
    .Handle(Exception)
    .Retry(5)
    .Wait(TTimeSpan.FromSeconds(1))  // Initial delay: 1 second
    .When(function(Context: TRetryContext): Boolean
          begin
            WriteLn(Format('Attempt %d failed. Waiting %d ms before retry...',
              [Context.Attempts, Trunc(Context.WaitDelay.TotalMilliseconds)]));

            // Double the delay for next retry (exponential backoff)
            Context.WaitDelay := TTimeSpan.FromMilliseconds(
              Context.WaitDelay.TotalMilliseconds * 2
            );

            Result := True;
          end)
    .Build;

  Policy.Execute(
    procedure
    begin
      MakeNetworkRequest;
    end);

  // Delays: 1s, 2s, 4s, 8s, 16s
end;

Example 4: Conditional Retry Based on Exception Details

var
  Policy: IRetryPolicy;
begin
  Policy := TRetryBuilder
    .Handle(EIdHTTPProtocolException)
    .Retry(5)
    .Wait(TTimeSpan.FromSeconds(2))
    .When(function(Context: TRetryContext): Boolean
          begin
            if Context.Exception is EIdHTTPProtocolException then
            begin
              var Ex := EIdHTTPProtocolException(Context.Exception);

              // Only retry on specific status codes
              case Ex.ErrorCode of
                408, // Request Timeout
                429, // Too Many Requests
                503, // Service Unavailable
                504: // Gateway Timeout
                  begin
                    WriteLn(Format('Received %d, retrying...', [Ex.ErrorCode]));
                    Result := True;
                  end;
              else
                WriteLn(Format('Received %d, not retrying', [Ex.ErrorCode]));
                Result := False;
              end;
            end
            else
              Result := True;
          end)
    .Build;

  Policy.Execute(
    procedure
    begin
      CallAPI;
    end);
end;

See Also


← Base Policy | API Reference Index | Circuit Breaker Policy →

Clone this wiki locally