| sidebar_key | resilience | ||
|---|---|---|---|
| tags |
|
Utilities for the two things that go wrong when work is spread across processes: something is not ready yet, and something must only happen once.
- Exponential backoff - poll for a value with a growing delay and a real ceiling on total wait time
- Distributed mutex - a named lock with owner tokens and TTL-based crash recovery
- Pluggable lock stores - built on an atomic compare-and-swap contract, not on best-effort caching
- Attribute-driven - attach either policy declaratively to a method
- Testable timing - injectable clock and sleep, so multi-second policies verify in virtual time
Wait for something that is not there yet:
use ByJG\Resilience\ExponentialBackoffWaiter;
$waiter = new ExponentialBackoffWaiter(
initialDelayMicros: 200_000, // 200ms
maxTotalWaitMicros: 3_500_000, // 3.5s
multiplier: 2.0
);
$reward = $waiter->wait(fn() => $rewardRepo->get($id));Make sure something happens only once:
use ByJG\Cache\Psr16\RedisCacheEngine;
use ByJG\Resilience\Lock\CacheEngineLockStore;
use ByJG\Resilience\Mutex;
$mutex = new Mutex(new CacheEngineLockStore(new RedisCacheEngine()));
$result = $mutex->execute('rebuild-report', fn() => rebuildTheReport(), ttlSeconds: 60);- Exponential Backoff - the waiter, its timing guarantees, and testing it
- Mutex - acquiring, releasing, refreshing, and choosing a TTL
- Lock Stores - the store contract, the shipped adapter, and what a lock does not guarantee
- Attributes -
#[ExponentialBackoff]and#[Mutex]with their executors
PHP 8.3 or later.
The Mutex needs a store that can perform an atomic set-if-absent. The shipped adapter covers
byjg/cache-engine engines backed by Redis or Memcached:
composer require byjg/cache-engineA plain PSR-16 cache is deliberately not accepted — see Lock Stores for why.
composer require byjg/resiliencevendor/bin/phpunitTests covering the lock stores need Redis and Memcached, and skip cleanly when they are absent:
docker compose up -d
vendor/bin/phpunit
docker compose down