gohany / rtry
Common implementation for retrying operations
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 1
Open Issues: 0
pkg:composer/gohany/rtry
Requires
- php: >=7.4
- ext-ctype: *
- gohany/retry: ^1.0
- psr/clock: ^1.0
- psr/http-message: ^2.0
- psr/log: ^3.0
Requires (Dev)
- phpunit/php-code-coverage: ^12.4
- phpunit/phpunit: ^12.3
This package is auto-updated.
Last update: 2026-01-02 05:54:36 UTC
README
Robust, composable retries for HTTP, RPC, queues, and DB work. Rtry focuses on clear policy specs, safe defaults, and first‑class support for jitter, hedging, deadlines, and rate‑limit headers.
Namespaces used in this repo include
Gohany\Rtry(implementation & policy) andGohany\Retry(interfaces / runtime). The library targets PHP 7.4+ and works on PHP 8.x.
Table of contents
- Features
- Install
- Quick start
- Core concepts
- Array spec: authoring policies concisely
- Hooks
- Duration tokens
- Testing & code coverage
- CI badges
- Examples
- Contributing
- License
The specification docs are in SPEC.md.
Features
- Simple API:
Retry::try(callable $op, RetryPolicyInterface $policy) - Expressive policies: compact array spec (
a,sa,dl,cap,j,h,seq,fh) or factory builders - Backoff modes with jitter (full / plus‑minus / none)
- Hedging (concurrent lanes with stagger & cancel policy)
- Global deadlines and per‑attempt timeouts
- Follow server hints:
Retry-After,RateLimit-Reset(-After), etc. - Deciders & rules to control retry on specific failures / tags / status families
- PSR friendly:
Psr\Clock\ClockInterface,Psr\Log\LoggerInterface,Psr\Http\Message\ResponseInterface - Thorough tests and examples
Install
Via Composer:
composer require Gohany/Rtry
Quick start
use Gohany\Rtry\Impl\Retry; use Gohany\Rtry\Impl\RtryPolicyFactory; // Build a policy from a string spec: $factory = new RtryPolicyFactory(); $policy = $factory->fromSpec('rtry:a=5;d=200;mode=exp;b=2;cap=8s;j=100@full;on=5xx,ETIMEDOUT'); $retry = new Retry(); // uses default Clock, Sleeper, Logger $result = $retry->try(function () { // Your flaky operation (HTTP call, DB query, etc) return 'ok'; }, $policy);
Core concepts
Retry runtime
Gohany\Rtry\Impl\Retry orchestrates attempts:
- asks the policy for startAfter, nextDelayMs(attempt), attemptTimeoutMs, deadlineBudgetMs, capMs, backoffMode
- applies jitter and cap
- optionally follows headers using the failure classifier (see below)
- respects deadlines and per‑attempt timeouts
- emits hooks: betweenAttempts, onGiveUp
Retry policy
Implemented by RetryPolicyInterface (base) and enriched by RtryPolicyInterface (jitter/hedge/backoff/classifier). Build via RtryPolicyFactory from an array spec or use the underlying parts (e.g., Attempts, Delay, Sequence, Jitter, Hedge).
Delays & sequences
Use a fixed backoff, exponential, or an explicit sequence.
use Gohany\Rtry\Impl\Parts\Sequence; // Accepts multiple forms: Sequence::make('50,100,1.5s*'); Sequence::make('(50,100ms,1.5s*)'); Sequence::make('seq=(50,100,1.5s,*)'); // Canonical string form: (string) Sequence::make('50,100,1.5s*'); // "seq=(50,100,1.5s*)" // '*' means repeat the last delay indefinitely.
Jitter
Jitter smooths out stampedes by randomizing delays.
JitterSpecInterface:
mode()—'full' | 'pm' | 'none'(pm = plus/minus)percent()— 0..1 (for plus/minus), ornullwindowMs()— absolute window (for plus/minus), ornullapply(int $nominalDelayMs, ?int $seed = null): int
Spec examples for 'j':
'50@full', '100ms@full', '10m@full', '20%@full',
'50@pm', '100ms@pm', '10m@pm', '20%@pm',
Hedging
Run multiple lanes concurrently with a stagger delay, cancel others on first success/completion.
HedgeSpecInterface:
getLanes(): intgetStaggerDelayMs(): intgetCancelPolicy(): int(CANCEL_ON_FIRST_SUCCESS=0orCANCEL_ON_FIRST_COMPLETION=1)
Spec examples for 'h':
'2@100ms', '2@10s', '2@1m', '2@1h',
'3@100&1' // lanes=3, stagger=100ms, cancel-on-first-completion
Deadlines vs per‑attempt timeout
- Global deadline (
dl) sets a total budget for the whole retry run (sleep time counts). - Per‑attempt timeout (
timeout) limits a single attempt duration.
If there’s not enough budget for the next delay, Retry gives up and triggers the onGiveUp hook.
Following rate limit headers
With fh=1 (followHeaders=true), Retry consults the failure classifier to extract hints from responses:
Retry-After: 1→ minimum next delay = 1000msRetry-After: Wed, 21 Oct 2015 07:28:00 GMT→ notBeforeUnixMsRateLimit-Reset(-After),X-RateLimit-Reset(-After),X-RateLimit-Reset-MSare recognized
The RateLimitBackoffRule returns FailureMetadata with minNextDelayMs or notBeforeUnixMs, and tags like RATE_LIMITED.
Deciders
Control whether to retry:
AlwaysRetryDeciderOnTokensDecider— retry on status codes (429,503), families (4xx,5xx), or named tags (RETRY-AFTER,TRANSIENT)CompositeDecider— OR composition (short‑circuit)
Rule‑based failure classification
RuleBasedFailureClassifier applies rules to a Throwable to derive status, tags, context patch, headers, and backoff hints:
InstanceOfRule— match on exception classMessageRegexRule— match on messageMethodStatusRule— pull status fromgetResponse(): ResponseInterfaceorgetCode()RateLimitBackoffRule— parse standard rate‑limit headers
Derived tags include ETIMEDOUT, ECONNRESET, NETWORK_ERROR, and DB DEADLOCK heuristics.
Rtry spec: authoring policies concisely
You can configure multiple candidate values per key (the factory will validate & choose). Common keys:
| Key | Meaning | Examples |
|---|---|---|
a |
attempts | 5, [5,10] |
sa |
start after | 200, '200ms', '200s', '2m', '1h' |
dl |
deadline budget | 200, '200ms', '2s', '1m' |
cap |
cap for attempts | '200ms', '2m', '1h' |
j |
jitter | '20%@pm', '100ms@pm', '50@full', '1.5m@full' |
h |
hedge | '2@100ms', '3@100&1' |
seq |
explicit delay sequence | '(50,100ms,1.5s*)', 'seq=(50,100,1.5s,*)', '50,100,1.5s*' (canonical prints as seq=…) |
fh |
follow headers (bool) | true / false |
b |
exponential base | 2, 2.5 |
on |
on tags | 5xx, 429, ratelimit, throttle, 4xx |
Tip: numbers without unit default to milliseconds.
1.5mand1.5sare supported; decimals forh/m/sare allowed.
Hooks
$retry->setBetweenAttemptsHook(function ( \Gohany\Retry\AttemptContextInterface $ctx, \Gohany\Retry\AttemptOutcomeInterface $outcome, \Gohany\Retry\RetryPolicyInterface $policy, int $sleepMs, array $lastHeaders ) { // observe sleep decision, metrics, logging, etc. }); $retry->setOnGiveUpHook(function ($ctx, $outcome, $policy, $headers) { // after the final failure before giving up });
Duration tokens
- Accepted suffixes:
ms,s,m,h - Bare numbers =
ms - Decimals supported for
s/m/h(e.g.,1.5s,2.5m) - Canonical formatting prefers compact, human‑readable units; sub‑second prints as bare
ms
Testing & code coverage
Run the test suite:
composer install
composer test
Note: The test suite is configured to generate code coverage reports. If you encounter "No code coverage driver available", you'll need to install either Xdebug or PCOV:
Installing a code coverage driver
Option 1: PCOV (recommended for performance)
pecl install pcov
Then add to your php.ini:
extension=pcov.so
Option 2: Xdebug
pecl install xdebug
Then add to your php.ini:
zend_extension=xdebug.so xdebug.mode=coverage
Run tests without coverage:
composer test -- --no-coverage
Examples
Retry with sequence + jitter + follow headers
$spec = 'rtry:a5;seq=50,100,200,400,800*;j=20%@pm'; $policy = (new RtryPolicyFactory())->fromSpec($spec); $result = (new Retry())->try(fn() => $client->get($url), $policy);
Hedged requests
$policy = (new RtryPolicyFactory())->fromSpec('rtry:a=1;h=3@75'); $retry = new Retry(); $resp = $retry->try(fn() => $client->get($url), $policy);
Deciders: retry on 4xx/5xx or named tags
use Gohany\Rtry\Impl\Deciders\CompositeDecider; use Gohany\Rtry\Impl\Deciders\OnTokensDecider; $spec = 'rtry:a5;seq=50,100*;j=20%@pm;on=4xx,5xx,NETWORK_ERROR,RATE_LIMITED'; $policy = (new RtryPolicyFactory())->fromSpec($spec); $retry = new Retry(); $resp = $retry->try(fn() => $client->get($url), $policy);
Contributing
- Run
composer test(orvendor/bin/phpunit) before PRs - Keep new rules/deciders covered with AAA‑style tests
- When adding new duration tokens or headers, document them here and in tests
License
MIT (see LICENSE).