diff --git a/src/CircuitBreaker.php b/src/CircuitBreaker.php index ca922e5..c3c7152 100755 --- a/src/CircuitBreaker.php +++ b/src/CircuitBreaker.php @@ -12,33 +12,36 @@ class CircuitBreaker protected IStoreProvider $store; + protected string $service; + public function __construct(Config $config, IStoreProvider $store) { $this->config = $config; $this->store = $store; + $this->service = $config->getServiceName(); } public function run(\Closure $action) { if ($this->isOpen()) { if (! $this->shouldBecomeHalfOpen()) { - throw CircuitOpenException::make($this->config->getServiceName()); + throw CircuitOpenException::make($this->service); } try { - $this->store->halfOpen(); + $this->store->halfOpen($this->service); $result = call_user_func($action); - $this->store->onSuccess($result); + $this->store->onSuccess($result, $this->service); } catch (\Exception $exception) { $this->openCircuit(); throw $exception; } - if ($this->store->counter()->getNumberOfSuccess() >= $this->config->numberOfSuccessToCloseState) { - $this->store->close(); + if ($this->store->counter($this->service)->getNumberOfSuccess() >= $this->config->numberOfSuccessToCloseState) { + $this->store->close($this->service); } return $result; @@ -47,7 +50,7 @@ public function run(\Closure $action) try { $result = call_user_func($action); - $this->store->onSuccess($result); + $this->store->onSuccess($result, $this->service); } catch (\Exception $exception) { $this->handleFailure($exception); @@ -59,17 +62,17 @@ public function run(\Closure $action) public function isOpen() { - return $this->store->state() != CircuitState::Closed; + return $this->store->state($this->service) != CircuitState::Closed; } public function shouldBecomeHalfOpen(): bool { - $lastChange = $this->store->lastChangedDateUtc(); + $lastChange = $this->store->lastChangedDateUtc($this->service); if ($lastChange) { $now = Carbon::now("UTC"); - $shouldBeHalfOpenAt = Carbon::parse($this->store->lastChangedDateUtc()) + $shouldBeHalfOpenAt = Carbon::parse($lastChange) ->timezone("UTC") ->addSeconds($this->config->openToHalfOpenWaitTime); @@ -83,17 +86,17 @@ public function handleFailure(\Exception $exception): void { // Log exception - $this->store->incrementFailure($exception); + $this->store->incrementFailure($exception, $this->service); // Open circuit if needed - if ($this->store->counter()->getNumberOfFailures() > $this->config->maxNumberOfFailures) { + if ($this->store->counter($this->service)->getNumberOfFailures() > $this->config->maxNumberOfFailures) { $this->openCircuit(); } } public function openCircuit(): void { - $this->store->open(); - $this->store->reset(); + $this->store->open($this->service); + $this->store->reset($this->service); } } diff --git a/src/Counter.php b/src/Counter.php index ccf5cd4..962edde 100644 --- a/src/Counter.php +++ b/src/Counter.php @@ -27,6 +27,12 @@ public function failure(): void $this->numberOfFailures++; } + public function reset() + { + $this->numberOfFailures = 0; + $this->numberOfSuccess = 0; + } + public function failurePercent(): float { return round($this->numberOfFailures / $this->totalTries(), 2); diff --git a/src/Stores/IStoreProvider.php b/src/Stores/IStoreProvider.php index 2e14e7e..614ae4a 100644 --- a/src/Stores/IStoreProvider.php +++ b/src/Stores/IStoreProvider.php @@ -7,21 +7,21 @@ interface IStoreProvider { - public function state(): CircuitState; + public function state($service): CircuitState; - public function lastChangedDateUtc(); + public function lastChangedDateUtc($service); - public function halfOpen(): void; + public function halfOpen($service): void; - public function open(): void; + public function open($service): void; - public function close(): void; + public function close($service): void; - public function counter(): Counter; + public function counter($service): Counter; - public function reset(); + public function reset($service); - public function onSuccess($result); + public function onSuccess($result, $service); - public function incrementFailure(\Exception $exception); + public function incrementFailure(\Exception $exception, $service); } diff --git a/src/Stores/RedisStore.php b/src/Stores/RedisStore.php index f74e27e..4da4e19 100644 --- a/src/Stores/RedisStore.php +++ b/src/Stores/RedisStore.php @@ -14,63 +14,92 @@ public function __construct(\Redis $redis) $this->redis = $redis; } - public function state(): CircuitState + public function state($service): CircuitState { - return $this->redis->get($this->getNamespace()); + $key = $this->getStateKey($service); + + $state = $this->redis->get($key); + + if (! $state) { + return CircuitState::Closed; + } + + return CircuitState::from($state); } - public function lastChangedDateUtc() + public function lastChangedDateUtc($service) { // TODO: Implement lastChangedDateUtc() method. } - public function halfOpen(): void + public function halfOpen($service): void { - $this->redis->set($this->getNamespace(), CircuitState::HalfOpen->value, 1000); + $this->redis->set($this->getStateKey($service), CircuitState::HalfOpen->value, 1000); } - public function open(): void + public function open($service): void { // ToDo Define timeouts - $this->redis->set($this->getNamespace(), CircuitState::Open->value, 1000); + $this->redis->set($this->getStateKey($service), CircuitState::Open->value, 1000); } - public function close(): void + public function close($service): void { - $this->redis->set($this->getNamespace(), CircuitState::Closed->value, 1000); + $this->redis->set($this->getStateKey($service), CircuitState::Closed->value, 1000); } - public function counter(): Counter + public function counter($service): Counter { - // TODO: Implement counter() method. + $key = $this->getCounterKey($service); + + $counter = $this->redis->get($key); + + if (! $counter) { + return new Counter(); + } + + return unserialize($counter); } - public function reset() + public function reset($service) { - // TODO: Implement reset() method. + $counter = $this->counter($service); + + $counter->reset(); + + $this->redis->set($this->getCounterKey($service), serialize($counter), 1000); } - public function onSuccess($result) + public function onSuccess($result, $service) { - // TODO: Implement onSuccess() method. + $counter = $this->counter($service); + + $counter->success(); + + $this->redis->set($this->getCounterKey($service), serialize($counter), 1000); } - public function incrementFailure(\Exception $exception) + public function incrementFailure(\Exception $exception, $service) { - $failuresKey = $this->getNamespace() . ':failure:counter'; + $counter = $this->counter($service); - if (! $this->redis->get($failuresKey)) { - $this->redis->multi(); - $this->redis->incr($failuresKey); + $counter->failure(); - return (bool) ($this->redis->exec()[0] ?? false); - } + $this->redis->set($this->getCounterKey($service), serialize($counter), 1000); + } - return (bool) $this->redis->incr($failuresKey); + protected function getCounterKey($service) + { + return $this->getKey($service) . ':counter'; + } + + protected function getStateKey($service) + { + return $this->getKey($service) . ":state"; } - public function getNamespace() + protected function getKey($service) { - return "stfn-circuit-breaker-package:{$this->service}"; + return "stfn-circuit-breaker-package:{$service}"; } } diff --git a/tests/CircuitBreakerTest.php b/tests/CircuitBreakerTest.php index 39257d6..7fcd03b 100644 --- a/tests/CircuitBreakerTest.php +++ b/tests/CircuitBreakerTest.php @@ -8,6 +8,7 @@ use Stfn\CircuitBreaker\CircuitState; use Stfn\CircuitBreaker\Config; use Stfn\CircuitBreaker\Exceptions\CircuitOpenException; +use Stfn\CircuitBreaker\Stores\RedisStore; use Stfn\CircuitBreaker\Tests\TestClasses\InMemoryStore; class CircuitBreakerTest extends TestCase @@ -63,8 +64,8 @@ public function test_if_it_will_record_every_success() $closure(); } - $this->assertEquals($tries, $store->counter()->getNumberOfSuccess()); - $this->assertEquals(0, $store->counter()->getNumberOfFailures()); + $this->assertEquals($tries, $store->counter('test-service')->getNumberOfSuccess()); + $this->assertEquals(0, $store->counter('test-service')->getNumberOfFailures()); } public function test_if_it_will_record_every_failure() @@ -93,8 +94,8 @@ public function test_if_it_will_record_every_failure() } } - $this->assertEquals($tries, $store->counter()->getNumberOfFailures()); - $this->assertEquals(0, $store->counter()->getNumberOfSuccess()); + $this->assertEquals($tries, $store->counter('test-service')->getNumberOfFailures()); + $this->assertEquals(0, $store->counter('test-service')->getNumberOfSuccess()); } public function test_if_it_will_open_circuit_after_failure_threshold() @@ -152,14 +153,14 @@ public function test_if_counter_is_reset_after_circuit_change_state_from_close_t } } - $this->assertEquals(0, $store->counter()->getNumberOfSuccess()); - $this->assertEquals(0, $store->counter()->getNumberOfFailures()); + $this->assertEquals(0, $store->counter('test-service')->getNumberOfSuccess()); + $this->assertEquals(0, $store->counter('test-service')->getNumberOfFailures()); } public function test_if_it_will_close_circuit_after_success_calls() { $store = new InMemoryStore(); - $store->open(); + $store->open('test-service'); Carbon::setTestNow(Carbon::yesterday()); @@ -182,7 +183,7 @@ public function test_if_it_will_close_circuit_after_success_calls() $closure(); } - $this->assertEquals(CircuitState::Closed, $store->state()); + $this->assertEquals(CircuitState::Closed, $store->state('test-service')); } public function test_if_it_will_transit_back_to_closed_state_after_first_fail() @@ -231,16 +232,31 @@ public function getDefaultConfig() return new Config("test-service"); } - // public function test_if_it_will_fail_after_percentage_threshold_for_failure() - // { + // public function test_if_it_will_fail_after_percentage_threshold_for_failure() + // { // - // } + // } // public function test_if_redis_work() // { // $redis = new \Redis(); // $redis->connect('127.0.0.1'); // - // $store = new RedisStore("test-circuit", $redis); - // $store->halfOpen(); + // $store = new RedisStore($redis); + // + // $config = Config::make('test-service', [ + // 'max_number_of_failures' => 3, + // ]); + // + // $circuitBreaker = new CircuitBreaker($config, $store); + // + // try { + // $result = $circuitBreaker->run(function () { + // throw new \Exception('test'); + // }); + // } catch (\Exception $exception) { + // dump($exception->getMessage()); + // } + // + // dd($store->counter('test-service')); // } } diff --git a/tests/TestClasses/InMemoryStore.php b/tests/TestClasses/InMemoryStore.php index c33c9f5..4a84997 100644 --- a/tests/TestClasses/InMemoryStore.php +++ b/tests/TestClasses/InMemoryStore.php @@ -20,51 +20,51 @@ public function __construct() $this->counter = new Counter(); } - public function state(): CircuitState + public function state($service): CircuitState { return $this->state; } - public function lastChangedDateUtc() + public function lastChangedDateUtc($service) { return $this->lastChangedDateUtc; } - public function halfOpen(): void + public function halfOpen($service): void { $this->state = CircuitState::HalfOpen; } - public function open(): void + public function open($service): void { $this->state = CircuitState::Open; $this->lastChangedDateUtc = Carbon::now("UTC")->toDateTimeString(); } - public function close(): void + public function close($service): void { $this->state = CircuitState::Closed; $this->lastChangedDateUtc = Carbon::now("UTC")->toDateTimeString(); } - public function counter(): Counter + public function counter($service): Counter { return $this->counter; } - public function reset() + public function reset($service) { $this->counter = new Counter(); } - public function onSuccess($result) + public function onSuccess($result, $service) { $this->counter->success(); } - public function incrementFailure(\Exception $exception) + public function incrementFailure(\Exception $exception, $service) { $this->counter->failure(); }