Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Retry Policies

Whim\Retry runs a callable again after a failure. A Policy sets the maximum attempt count, delays, jitter, error condition, and optional deadline.

use Whim\Reference\Strong;
use Whim\Retry;
use Whim\Retry\Policy;
use Whim\Unwind\RuntimeException;

$calls = new Strong::<int>(0);
$result = Retry\retry::<string>(Policy::attempts(3u), fn(): string {
  $calls->value++;
  if ($calls->value < 3) {
    throw new RuntimeException('try again');
  }

  return 'done';
});

assert!($result == 'done');
assert!($calls->value == 3);

The count includes the first attempt. Policy::attempts(1u) runs once. By default, a policy has no delay and retries only Whim\Unwind\Exception and its subclasses. It does not retry an Error. When retries stop, the retrier throws the last throwable unchanged.

Delays and conditions

Policy methods return copies:

MethodEffect
withDelay(Delay)Uses NoDelay, FixedDelay, or ExponentialDelay.
withFixedDelay(Duration)Uses the same delay after each failed attempt.
withExponentialDelay(initial, multiplier = 2u, maximum = null)Multiplies the delay after each failed attempt, with an optional cap.
withJitter(Jitter)Uses the exact delay (None) or a random delay from zero to that delay (Full).
withCondition(fn(Throwable): bool)Replaces the rule that decides which errors to retry.
withDeadline(Duration)Stops after a failure if elapsed time exceeds the deadline or the next delay would pass it.

Attempt counts must be positive. Delays and deadlines may be zero but cannot be negative. An exponential multiplier must be at least two. An uncapped delay that grows beyond Duration’s range throws OverflowError.

The deadline does not interrupt an operation already running. Use an operation’s own timeout or cancellation support to bound that call. A cancellation token passed to retry or Retrier::run cancels retry pauses; the operation must handle its own cancellation.

Controlled time

Retrier accepts a Whim\Clock\Sleeper, a Whim\Clock\Clock, and a Whim\RandomSequence\Sequence. It uses SystemSleeper, SystemClock, and a securely seeded sequence by default.

A frozen clock and virtual sleeper let tests check delays without waiting:

use Whim\Clock\FrozenClock;
use Whim\Clock\VirtualSleeper;
use Whim\RandomSequence\MersenneTwisterSequence;
use Whim\Reference\Strong;
use Whim\Retry\Jitter;
use Whim\Retry\Policy;
use Whim\Retry\Retrier;
use Whim\Time\Duration;
use Whim\Unwind\RuntimeException;

$clock = FrozenClock::atUnixTimestamp(0);
$retrier = new Retrier(
  new VirtualSleeper($clock),
  $clock,
  new MersenneTwisterSequence(7u),
);
$policy = Policy::attempts(4u)
  ->withExponentialDelay(Duration::second())
  ->withJitter(Jitter::None)
  ->withDeadline(Duration::fromSeconds(10));

$calls = new Strong::<int>(0);
$result = $retrier->run::<int>($policy, fn(): int {
  $calls->value++;
  if ($calls->value < 4) {
    throw new RuntimeException('try again');
  }

  return 42;
});

assert!($result == 42);
assert!($clock->now()->getSeconds() == 7);

See Clocks and Sleepers, Circuit Breakers, and Rate Limits.