Skip to content

Commit

Permalink
Add listeners and skip callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
stfndamjanovic committed Jan 11, 2024
1 parent 7df379a commit 03f941a
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 17 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,18 @@ composer require stfndamjanovic/php-circuit-breaker

```php
use Stfn\CircuitBreaker\CircuitBreakerFactory;
use Stfn\CircuitBreaker\Exceptions\CircuitHalfOpenFailException;

$result = CircuitBreakerFactory::make()
->for('test-service')
->failWhen(function ($result) {
if ($result->status > 400) {
throw new Exception();
}
})
->skipFailure(function (Exception $exception) {
return $exception instanceof CircuitHalfOpenFailException;
})
->withOptions([
'recovery_time' => 30,
'failure_threshold' => 5
Expand Down
26 changes: 26 additions & 0 deletions src/CircuitBreaker.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Stfn\CircuitBreaker\StateHandlers\ClosedStateHandler;
use Stfn\CircuitBreaker\StateHandlers\HalfOpenStateHandler;
use Stfn\CircuitBreaker\StateHandlers\OpenStateHandler;
use Stfn\CircuitBreaker\StateHandlers\StateHandler;
use Stfn\CircuitBreaker\Storage\CircuitBreakerStorage;
use Stfn\CircuitBreaker\Storage\InMemoryStorage;

Expand All @@ -21,6 +22,21 @@ class CircuitBreaker
*/
public CircuitBreakerStorage $storage;

/**
* @var CircuitBreakerListener[]
*/
public array $listeners = [];

/**
* @var \Closure|null
*/
public \Closure|null $failWhenCallback = null;

/**
* @var \Closure|null
*/
public \Closure|null $skipFailureCallback = null;

/**
* @param Config|null $config
* @param CircuitBreakerStorage|null $storage
Expand All @@ -39,6 +55,7 @@ public function __construct(Config $config = null, CircuitBreakerStorage $storag
*/
public function call(\Closure $action, ...$args)
{
/** @var StateHandler $stateHandler */
$stateHandler = $this->makeStateHandler();

return $stateHandler->call($action, $args);
Expand Down Expand Up @@ -88,4 +105,13 @@ public function isOpen()
{
return $this->storage->getState() !== CircuitState::Closed;
}

/**
* @param CircuitBreakerListener $listener
* @return void
*/
public function addListener(CircuitBreakerListener $listener)
{
$this->listeners[] = $listener;
}
}
27 changes: 25 additions & 2 deletions src/CircuitBreakerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

class CircuitBreakerFactory
{
protected CircuitBreaker $circuitBreaker;
public CircuitBreaker $circuitBreaker;

public static function make()
{
Expand All @@ -23,13 +23,36 @@ public function for(string $service)
return $this;
}

public function withOptions(array $options)
public function withOptions(array $options): self
{
$this->circuitBreaker->config = Config::make($options);

return $this;
}

public function withListeners(array $listeners): self
{
foreach ($listeners as $listener) {
$this->circuitBreaker->addListener($listener);
}

return $this;
}

public function skipFailure(\Closure $closure)
{
$this->circuitBreaker->skipFailureCallback = $closure;

return $this;
}

public function failWhen(\Closure $closure)
{
$this->circuitBreaker->failWhenCallback = $closure;

return $this;
}

public function storage(CircuitBreakerStorage $storage)
{
$this->circuitBreaker->storage = $storage;
Expand Down
16 changes: 16 additions & 0 deletions src/CircuitBreakerListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Stfn\CircuitBreaker;

class CircuitBreakerListener
{
public function onSuccess($result): void
{

}

public function onFail($exception): void
{

}
}
31 changes: 26 additions & 5 deletions src/StateHandlers/StateHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function call(\Closure $action, ...$args)
try {
$result = call_user_func($action, $args);

$this->handleSucess();
$this->handleSucess($result);
} catch (\Exception $exception) {
$this->handleFailure($exception);
}
Expand All @@ -53,21 +53,42 @@ public function beforeCall(\Closure $action, ...$args)

/**
* @param \Exception $exception
* @return void
* @return mixed
* @throws \Exception
*/
public function handleFailure(\Exception $exception)
{
//@ToDO Add listeners here
if (is_callable($this->breaker->skipFailureCallback)) {
$shouldSkip = call_user_func($this->breaker->skipFailureCallback, $exception);

if ($shouldSkip) {
return;
}
}

foreach ($this->breaker->listeners as $listener) {
$listener->onFail($exception);
}

$this->onFailure($exception);

throw $exception;
}

/**
* @return void
*/
public function handleSucess()
public function handleSucess($result)
{
// @ToDo Add listeners here
if (is_callable($this->breaker->failWhenCallback)) {
call_user_func($this->breaker->failWhenCallback, $result);
}

$this->onSucess();

foreach ($this->breaker->listeners as $listener) {
$listener->onSuccess($result);
}
}

/**
Expand Down
10 changes: 2 additions & 8 deletions src/Storage/RedisStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,16 @@ class RedisStorage extends CircuitBreakerStorage
*/
protected \Redis $redis;

/**
* @var
*/
protected string $namespace;

/**
* @param string $service
* @param \Redis $redis
* @throws \RedisException
*/
public function __construct(string $namespace, string $service, \Redis $redis)
public function __construct(\Redis $redis, string $service)
{
parent::__construct($service);

$this->redis = $redis;
$this->namespace = $namespace;

$this->initState();
}
Expand Down Expand Up @@ -109,7 +103,7 @@ public function openedAt(): int
*/
protected function getNamespace(string $key): string
{
$tags = [self::BASE_NAMESPACE, $this->namespace, $this->service, $key];
$tags = [self::BASE_NAMESPACE, $this->service, $key];

return join(":", $tags);
}
Expand Down
85 changes: 83 additions & 2 deletions tests/CircuitBreakerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

use PHPUnit\Framework\TestCase;
use Stfn\CircuitBreaker\CircuitBreaker;
use Stfn\CircuitBreaker\CircuitBreakerFactory;
use Stfn\CircuitBreaker\CircuitBreakerListener;
use Stfn\CircuitBreaker\CircuitState;
use Stfn\CircuitBreaker\Config;
use Stfn\CircuitBreaker\Exceptions\CircuitHalfOpenFailException;
use Stfn\CircuitBreaker\Exceptions\CircuitOpenException;
use Stfn\CircuitBreaker\Storage\RedisStorage;

class CircuitBreakerTest extends TestCase
{
Expand Down Expand Up @@ -75,7 +76,11 @@ public function test_if_it_will_record_every_failure()
$tries = 3;

foreach (range(1, $tries) as $i) {
$breaker->call($fail);
try {
$breaker->call($fail);
} catch (\Exception) {

}
}

$this->assertEquals($tries, $breaker->storage->getFailuresCount());
Expand Down Expand Up @@ -162,6 +167,82 @@ public function test_if_it_will_transit_back_to_open_state_after_first_fail()
$this->assertTrue($breaker->isOpen());
}

public function test_if_listener_is_called()
{
$object = new class extends CircuitBreakerListener {
public int $successCount = 0;
public int $failCount = 0;

public function onSuccess($result): void
{
$this->successCount++;
}

public function onFail($exception): void
{
$this->failCount++;
}
};

$factory = CircuitBreakerFactory::make()
->withOptions(['failure_threshold' => 10])
->withListeners([$object]);

$success = function () {
return true;
};

$fail = function () {
throw new \Exception();
};

$factory->call($success);
$factory->call($success);

try {
$factory->call($fail);
} catch (\Exception) {

}

$this->assertEquals(2, $object->successCount);
$this->assertEquals(1, $object->failCount);
}

public function test_if_it_can_skip_some_exception()
{
$testException = new class extends \Exception {};

$factory = CircuitBreakerFactory::make()
->skipFailure(function (\Exception $exception) use ($testException) {
return $exception instanceof $testException;
});

$factory->call(function () use ($testException) {
throw $testException;
});

$this->assertEquals(0, $factory->circuitBreaker->storage->getFailuresCount());
}

public function test_if_it_can_fail_even_without_exception()
{
$factory = CircuitBreakerFactory::make()
->failWhen(function ($result) {
if ($result instanceof \stdClass) {
throw new \Exception();
}
});

try {
$factory->call(fn() => new \stdClass());
} catch (\Exception) {

}

$this->assertEquals(1, $factory->circuitBreaker->storage->getFailuresCount());
}

// public function test_if_redis_work()
// {
// $redis = new \Redis();
Expand Down

0 comments on commit 03f941a

Please sign in to comment.