diff --git a/README.md b/README.md index 02cd677..8801adf 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ composer require leocarmo/circuit-breaker-php ## Adapters - [Redis](#redis-adapter) +- [Redis Cluster](#redis-cluster-adapter) - [Swoole Table](#swooletable-adapter) ### Redis Adapter @@ -36,6 +37,26 @@ $circuit = new CircuitBreaker($adapter, 'my-service'); > See [this](examples/RedisAdapterExample.php) for full example +### Redis Cluster Adapter +Without use of [`multi`](https://redis.io/commands/multi/) command. +The first argument is a redis connection, the second is your product name, for redis namespace avoid key conflicts with another product using the same redis. + +```php +use LeoCarmo\CircuitBreaker\CircuitBreaker; +use LeoCarmo\CircuitBreaker\Adapters\RedisClusterAdapter; + +// Connect to redis +$redis = new \Redis(); +$redis->connect('localhost', 6379); + +$adapter = new RedisClusterAdapter($redis, 'my-product'); + +// Set redis adapter for CB +$circuit = new CircuitBreaker($adapter, 'my-service'); +``` + +> See [this](examples/RedisClusterAdapterExample.php) for full example + ### SwooleTable Adapter ```php diff --git a/examples/RedisClusterAdapterExample.php b/examples/RedisClusterAdapterExample.php new file mode 100644 index 0000000..2c3d755 --- /dev/null +++ b/examples/RedisClusterAdapterExample.php @@ -0,0 +1,44 @@ +connect('localhost', 6379); + +$adapter = new RedisClusterAdapter($redis, 'my-product'); + +// Set redis adapter for CB +$circuit = new CircuitBreaker($adapter, 'my-service'); + +// Configure settings for CB +$circuit->setSettings([ + 'timeWindow' => 60, // Time for an open circuit (seconds) + 'failureRateThreshold' => 50, // Fail rate for open the circuit + 'intervalToHalfOpen' => 30, // Half open time (seconds) +]); + +// Check circuit status for service +if (! $circuit->isAvailable()) { + die('Circuit is not available!'); +} + +// Usage example for success and failure +function myService() { + if (rand(1, 100) >= 50) { + throw new RuntimeException('Something got wrong!'); + } +} + +try { + myService(); + $circuit->success(); + echo 'success!' . PHP_EOL; +} catch (RuntimeException $e) { + // If an error occurred, it must be recorded as failure. + $circuit->failure(); + echo 'fail!' . PHP_EOL; +} \ No newline at end of file diff --git a/src/Adapters/RedisClusterAdapter.php b/src/Adapters/RedisClusterAdapter.php new file mode 100644 index 0000000..eb84a86 --- /dev/null +++ b/src/Adapters/RedisClusterAdapter.php @@ -0,0 +1,146 @@ +checkExtensionLoaded(); + $this->redis = $redis; + $this->redisNamespace = $redisNamespace; + } + + protected function checkExtensionLoaded(): void + { + if (! extension_loaded('redis')) { + throw new \RuntimeException('Extension redis is required to use RedisAdapter.'); + } + } + + /** + * @param string $service + * @return bool + */ + public function isOpen(string $service): bool + { + return (bool) $this->redis->get( + $this->makeNamespace($service) . ':open' + ); + } + + /** + * @param string $service + * @param int $failureRateThreshold + * @return bool + */ + public function reachRateLimit(string $service, int $failureRateThreshold): bool + { + $failures = (int) $this->redis->get( + $this->makeNamespace($service) . ':failures' + ); + + return ($failures >= $failureRateThreshold); + } + + /** + * @param string $service + * @return bool + */ + public function isHalfOpen(string $service): bool + { + return (bool) $this->redis->get( + $this->makeNamespace($service) . ':half_open' + ); + } + + /** + * @param string $service + * @param int $timeWindow + * @return bool + */ + public function incrementFailure(string $service, int $timeWindow): bool + { + $serviceName = $this->makeNamespace($service) . ':failures'; + + if (! $this->redis->get($serviceName)) { + $this->redis->incr($serviceName); + return (bool) $this->redis->expire($serviceName, $timeWindow); + } + + return (bool) $this->redis->incr($serviceName); + } + + /** + * @param string $service + */ + public function setSuccess(string $service): void + { + $serviceName = $this->makeNamespace($service); + + $this->redis->del($serviceName . ':open'); + $this->redis->del($serviceName . ':failures'); + $this->redis->del($serviceName . ':half_open'); + } + + /** + * @param string $service + * @param int $timeWindow + */ + public function setOpenCircuit(string $service, int $timeWindow): void + { + $this->redis->set( + $this->makeNamespace($service) . ':open', + time(), + $timeWindow + ); + } + + /** + * @param string $service + * @param int $timeWindow + * @param int $intervalToHalfOpen + */ + public function setHalfOpenCircuit(string $service, int $timeWindow, int $intervalToHalfOpen): void + { + $this->redis->set( + $this->makeNamespace($service) . ':half_open', + time(), + ($timeWindow + $intervalToHalfOpen) + ); + } + + public function getFailuresCounter(string $service): int + { + $failures = $this->redis->get( + $this->makeNamespace($service) . ':failures' + ); + + return (int) $failures; + } + + /** + * @param string $service + * @return string + */ + protected function makeNamespace(string $service): string + { + return 'circuit-breaker:' . $this->redisNamespace . ':' . $service; + } +} diff --git a/tests/AdaptersTest.php b/tests/AdaptersTest.php index a7696fb..18ee403 100644 --- a/tests/AdaptersTest.php +++ b/tests/AdaptersTest.php @@ -1,6 +1,7 @@ connect(getenv('REDIS_HOST')); + $adapter = new RedisClusterAdapter($redis, 'my-product'); + + $this->assertInstanceOf(AdapterInterface::class, $adapter); + + return $adapter; + } + public function testCreateSwooleTableAdapter() { $adapter = new SwooleTableAdapter(); @@ -30,6 +42,7 @@ public function provideAdapters() { return [ 'redis' => [$this->testCreateRedisAdapter()], + 'redis-cluster' => [$this->testCreateRedisClusterAdapter()], 'swoole-table' => [$this->testCreateSwooleTableAdapter()], ]; }