syastrebov / circuit-breaker
Circuit breaker wrapper for microservices and api calls.
v0.0.9
2026-02-06 14:32 UTC
Requires
- php: ^8.3
- psr/log: ^3.0
Requires (Dev)
- ext-memcached: *
- ext-pdo: *
- ext-redis: *
- monolog/monolog: ^3.10
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.5
- predis/predis: ^3.3
- squizlabs/php_codesniffer: ^4.0
- vimeo/psalm: ^6.15
README
PHP Circuit Breaker implementation for microservices and API calls.
- Laravel package: https://github.com/syastrebov/laravel-circuit-breaker
- Symfony package: https://github.com/syastrebov/circuit-breaker-bundle
Install
composer require syastrebov/circuit-breaker
Usage
Simple usage:
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\RedisProvider; $redis = new \Redis(); $redis->connect('redis'); $circuit = new CircuitBreaker(new RedisProvider($redis)); $response = $circuit->run( $name, function () { // call your api return '{"response": "data"}'; } ); // {"response": "data"} echo $response;
Use fallback:
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\RedisProvider; $redis = new \Redis(); $redis->connect('redis'); $circuit = new CircuitBreaker(new RedisProvider($redis)); $response = $circuit->run( $name, // action function () { throw new \RuntimeException('unable to fetch data'); }, // fallback function () { // call your api return '{"response": "cached data"}'; } ); // {"response": "cached data"} echo $response;
Use exception:
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\RedisProvider; use CircuitBreaker\Exceptions; $redis = new \Redis(); $redis->connect('redis'); $circuit = new CircuitBreaker(new RedisProvider($redis)); try { $response = $circuit->run( $name, // action function () { throw new \RuntimeException('unable to fetch data'); } ); } catch (UnableToProcessException $e) { // handle exception }
Config
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\RedisProvider; use CircuitBreaker\CircuitBreakerConfig; $redis = new \Redis(); $redis->connect('redis'); $circuit = new CircuitBreaker(new RedisProvider($redis), new CircuitBreakerConfig( // Prefix prefix: 'api', // Number of attempts within run() action retries: 5, // Number of failed attempts to change state to 'OPEN' closedThreshold: 2, // Number of succeed attempts to change state to 'CLOSED' halfOpenThreshold: 2, // Delay between retries within run() action in microseconds retryInterval: 1000, // TTL of OPEN state in seconds openTimeout: 60, // If true and no fallback defined returns NULL otherwise throws UnableToProcessException fallbackOrNull: true ));
Supported Drivers
Redis:
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\RedisProvider; $redis = new \Redis(); $redis->connect('redis'); $circuit = new CircuitBreaker(new RedisProvider($redis));
Redis cluster:
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\RedisProvider; $redis = new \RedisCluster( 'my cluster', [ 'redis-node-1:6379', 'redis-node-2:6379', 'redis-node-3:6379', ], 1.5, 1.5, true ); $circuit = new CircuitBreaker(new RedisProvider($redis));
Predis:
use CircuitBreaker\CircuitBreaker; use Predis\Client; $predis = new Client([ 'host' => 'redis', ]); $predis->connect(); $provider = new PredisProvider($predis);
Predis cluster:
use CircuitBreaker\CircuitBreaker; use Predis\Client; $nodes = [ 'redis-node-1:6379', 'redis-node-2:6379', 'redis-node-3:6379', ]; $options = [ // 'redis' (server-side) or 'predis' (client-side) 'cluster' => 'redis', ]; $predis = new Client($nodes, $options); $predis->connect(); $provider = new PredisProvider($predis);
Memcached:
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\MemcachedProvider; $memcached = new \Memcached(); $memcached->addServer('memcached', 11211); $circuit = new CircuitBreaker(new MemcachedProvider($memcached));
MySQL:
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\DatabaseProvider; $table = 'circuit_breaker'; $pdo = new \PDO("mysql:host=mysql;dbname=database", 'user', 'password'); $pdo->prepare(" CREATE TABLE IF NOT EXISTS $table ( prefix VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, state ENUM('closed', 'open', 'half_open'), state_timestamp INT, half_open_attempts INT, failed_attempts INT, CONSTRAINT prefix_name_unique UNIQUE (prefix, name) ); ")->execute(); $provider = new DatabaseProvider($pdo, $table);
PostgreSQL:
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\DatabaseProvider; $table = 'circuit_breaker'; $pdo = new \PDO("pgsql:host=postgres;dbname=database", 'user', 'password'); $pdo->prepare(" DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'state_enum') THEN CREATE TYPE state_enum AS ENUM ('closed', 'open', 'half_open'); END IF; END $$; ")->execute(); $pdo->prepare(" CREATE TABLE IF NOT EXISTS $table ( prefix VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, state state_enum NULL, state_timestamp INT, half_open_attempts INT, failed_attempts INT, CONSTRAINT prefix_name_unique UNIQUE (prefix, name) ); ")->execute(); $provider = new DatabaseProvider($pdo, $table);
SQLite:
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\DatabaseProvider; $table = 'circuit_breaker'; $databaseFile = __DIR__ . '/database.sqlite'; $pdo = new \PDO("sqlite:$databaseFile"); $pdo->prepare(" CREATE TABLE IF NOT EXISTS $table ( prefix VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, state TEXT CHECK(state IN ('open', 'half_open', 'closed')), state_timestamp INTEGER, half_open_attempts INTEGER, failed_attempts INTEGER, CONSTRAINT prefix_name_unique UNIQUE (prefix, name) ); ")->execute(); $provider = new DatabaseProvider($pdo, $table);
Logger (optional)
use CircuitBreaker\CircuitBreaker; use CircuitBreaker\Providers\RedisProvider; use Monolog\Logger; $circuit = new CircuitBreaker( provider: new MemoryProvider(), logger: new Logger('circuit_breaker_channel') );
Run tests
docker compose up -d
docker compose exec -t php vendor/bin/phpunit