Skip to content

Commit

Permalink
Redis store wip
Browse files Browse the repository at this point in the history
  • Loading branch information
stfndamjanovic committed Dec 4, 2023
1 parent 6a82d65 commit 4d14ef3
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 69 deletions.
29 changes: 16 additions & 13 deletions src/CircuitBreaker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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);

Expand All @@ -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);
}
}
6 changes: 6 additions & 0 deletions src/Counter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 9 additions & 9 deletions src/Stores/IStoreProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
79 changes: 54 additions & 25 deletions src/Stores/RedisStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
}
}
42 changes: 29 additions & 13 deletions tests/CircuitBreakerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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());

Expand All @@ -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()
Expand Down Expand Up @@ -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'));
// }
}
Loading

0 comments on commit 4d14ef3

Please sign in to comment.