Circuit breaker pattern with configurable thresholds and multiple storage backends
composer require philiprehberger/php-circuit-breakerCircuit breaker pattern with configurable thresholds and multiple storage backends.
composer require philiprehberger/php-circuit-breaker
use PhilipRehberger\CircuitBreaker\CircuitBreaker;
use PhilipRehberger\CircuitBreaker\CircuitConfig;
use PhilipRehberger\CircuitBreaker\Storage\InMemoryStorage;
$breaker = new CircuitBreaker(
service: 'payment-api',
config: new CircuitConfig(
failureThreshold: 5,
recoveryTimeout: 30,
successThreshold: 1,
),
storage: new InMemoryStorage(),
);
$result = $breaker->call(function () {
return file_get_contents('https://api.example.com/charge');
});
use PhilipRehberger\CircuitBreaker\CircuitBreaker;
use PhilipRehberger\CircuitBreaker\Storage\FileStorage;
$breaker = CircuitBreaker::for('payment-api')
->failAfter(3)
->recoverAfter(60)
->storage(new FileStorage('/tmp/circuits'))
->onStateChange(function ($event, $breaker) {
echo "Circuit event: {$event->value}\n";
})
->build();
try {
$result = $breaker->call(fn () => callExternalService());
} catch (\PhilipRehberger\CircuitBreaker\CircuitOpenException $e) {
// Circuit is open, use fallback
}
Use KeyedCircuitBreaker to manage independent circuit breakers per key:
use PhilipRehberger\CircuitBreaker\KeyedCircuitBreaker;
use PhilipRehberger\CircuitBreaker\CircuitConfig;
use PhilipRehberger\CircuitBreaker\Storage\InMemoryStorage;
$breakers = new KeyedCircuitBreaker(
config: new CircuitConfig(failureThreshold: 3, recoveryTimeout: 60),
storage: new InMemoryStorage(),
);
$userResult = $breakers->call('user-api', fn () => fetchUsers());
$orderResult = $breakers->call('order-api', fn () => fetchOrders());
Register a fallback to return a default value when the circuit is open instead of throwing:
$breaker = CircuitBreaker::for('payment-api')
->failAfter(3)
->recoverAfter(60)
->fallback(fn () => ['status' => 'unavailable'])
->build();
// Returns the fallback value when the circuit is open
$result = $breaker->call(fn () => callExternalService());
Register a health check probe for smarter recovery from the Open state:
$breaker = new CircuitBreaker(
service: 'payment-api',
config: new CircuitConfig(failureThreshold: 3, recoveryTimeout: 30),
);
$breaker->setHealthCheck(function (): bool {
// Check if the external service is reachable
return @file_get_contents('https://api.example.com/health') !== false;
});
When the recovery timeout elapses, the health check runs before transitioning to HalfOpen. If the probe fails, the recovery timer resets.
Inspect detailed metrics about circuit breaker usage:
$breaker = new CircuitBreaker('payment-api');
$breaker->call(fn () => fetchData());
$metrics = $breaker->metrics();
// [
// 'total_calls' => 1,
// 'successful_calls' => 1,
// 'failed_calls' => 0,
// 'success_rate' => 1.0,
// 'current_state' => 'closed',
// 'state_changed_at' => null,
// 'consecutive_failures' => 0,
// ]
Inspect circuit breaker usage statistics at any time:
$breaker = new CircuitBreaker('payment-api');
$breaker->call(fn () => fetchData());
$stats = $breaker->getStats();
// [
// 'total_calls' => 1,
// 'successes' => 1,
// 'failures' => 0,
// 'last_failure_at' => null,
// 'current_state' => 'closed',
// ]
use PhilipRehberger\CircuitBreaker\Contracts\Storage;
use PhilipRehberger\CircuitBreaker\CircuitState;
class RedisStorage implements Storage
{
public function getState(string $service): CircuitState { /* ... */ }
public function setState(string $service, CircuitState $state): void { /* ... */ }
public function incrementFailures(string $service): int { /* ... */ }
public function resetFailures(string $service): void { /* ... */ }
public function getFailureCount(string $service): int { /* ... */ }
public function getLastFailureTime(string $service): ?float { /* ... */ }
public function setLastFailureTime(string $service, float $time): void { /* ... */ }
}
| Method | Description |
|---|---|
new CircuitBreaker(string $service, CircuitConfig $config, Storage $storage) | Create a circuit breaker |
CircuitBreaker::for(string $service) | Create a fluent builder |
->call(callable $fn): mixed | Execute a callable through the circuit breaker |
->isOpen(): bool | Check if the circuit is open |
->isClosed(): bool | Check if the circuit is closed |
->isHalfOpen(): bool | Check if the circuit is half-open |
->state(): CircuitState | Get the current circuit state |
->reset(): void | Reset the circuit to closed |
->trip(): void | Manually open the circuit |
->getStats(): array | Get usage statistics (total calls, successes, failures, last failure timestamp, current state) |
->metrics(): array | Get detailed metrics (total, successful, failed calls, success rate, state, state change time, consecutive failures) |
->setFallback(callable $fallback): void | Register a fallback invoked when the circuit is open |
->setHealthCheck(callable $probe): self | Register a health check probe for smarter Open-to-HalfOpen recovery |
| Method | Description |
|---|---|
->failAfter(int $failures): self | Set the failure threshold |
->recoverAfter(int $seconds): self | Set the recovery timeout |
->succeedAfter(int $successes): self | Set the success threshold for half-open |
->timeout(float $seconds): self | Set the call timeout |
->storage(Storage $storage): self | Set the storage backend |
->fallback(callable $fallback): self | Register a fallback for when the circuit is open |
->onStateChange(callable $callback): self | Set a state change callback |
->build(): CircuitBreaker | Build the configured instance |
| Method | Description |
|---|---|
new KeyedCircuitBreaker(CircuitConfig $config, Storage $storage) | Create a keyed circuit breaker manager |
->call(string $key, callable $fn): mixed | Execute through the breaker for the given key |
->state(string $key): CircuitState | Get the state for a specific key |
->isOpen(string $key): bool | Check if the circuit for a key is open |
->isClosed(string $key): bool | Check if the circuit for a key is closed |
->isHalfOpen(string $key): bool | Check if the circuit for a key is half-open |
->reset(string $key): void | Reset the circuit for a specific key |
->trip(string $key): void | Manually open the circuit for a specific key |
->remove(string $key): void | Remove the breaker for a key entirely |
->keys(): string[] | Get all tracked keys |
->count(): int | Get the number of tracked keys |
| Parameter | Type | Default | Description |
|---|---|---|---|
failureThreshold | int | 5 | Failures before opening |
recoveryTimeout | int | 30 | Seconds before half-open transition |
successThreshold | int | 1 | Successes in half-open to close |
timeout | ?float | null | Optional call timeout in seconds |
| Class | Description |
|---|---|
InMemoryStorage | In-memory, scoped to the current process |
FileStorage | JSON file-based, persists across requests |
SlidingWindowStorage | Time-window-based failure tracking with automatic pruning |
composer install
vendor/bin/phpunit
vendor/bin/pint --test
If you find this project useful: