Skip to content

Commit

Permalink
Improve transition from half open to closed state
Browse files Browse the repository at this point in the history
  • Loading branch information
stfndamjanovic committed Jan 25, 2024
1 parent 79f668f commit d2be5fd
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 48 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ use Stfn\CircuitBreaker\CircuitBreaker;

$breaker = CircuitBreaker::for('3rd-party-service')
->withOptions([
'failure_threshold' => 10,
'recovery_time' => 120,
'sample_duration' => 60,
'failure_threshold' => 10, // Number of failures triggering the transition to the open state
'recovery_time' => 120, // Time in seconds to keep the circuit breaker open before attempting recovery
'sample_duration' => 60, // Duration in seconds within which failures are counted
'consecutive_success' => 3 // Number of consecutive successful calls required to transition from half open to closed state
]);
```

Expand Down
14 changes: 12 additions & 2 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,27 @@ class Config
*/
public int $sampleDuration;

/**
* @var int
*/
public int $consecutiveSuccess;

/**
* @param int $failureThreshold
* @param int $recoveryTime
* @param int $sampleDuration
* @param int $consecutiveSuccesses
*/
public function __construct(
int $failureThreshold = 5,
int $recoveryTime = 60,
int $sampleDuration = 120
int $sampleDuration = 120,
int $consecutiveSuccess = 3
) {
$this->failureThreshold = $failureThreshold;
$this->recoveryTime = $recoveryTime;
$this->sampleDuration = $sampleDuration;
$this->consecutiveSuccess = $consecutiveSuccess;
}

/**
Expand All @@ -45,7 +53,8 @@ public static function fromArray(array $config = []): Config
return new Config(
$config['failure_threshold'] ?? 5,
$config['recovery_time'] ?? 60,
$config['sample_duration'] ?? 120
$config['sample_duration'] ?? 120,
$config['consecutive_success'] ?? 3
);
}

Expand All @@ -58,6 +67,7 @@ public function toArray()
'failure_threshold' => $this->failureThreshold,
'recovery_time' => $this->recoveryTime,
'sample_duration' => $this->sampleDuration,
'consecutive_success' => $this->consecutiveSuccess,
];
}
}
42 changes: 42 additions & 0 deletions src/Counter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace Stfn\CircuitBreaker;

class Counter
{
/**
* @var int
*/
protected int $success;

/**
* @var int
*/
protected int $failures;

/**
* @param int $success
* @param int $failures
*/
public function __construct(int $success, int $failures)
{
$this->success = $success;
$this->failures = $failures;
}

/**
* @return int
*/
public function numberOfSuccess()
{
return $this->success;
}

/**
* @return int
*/
public function numberOfFailures()
{
return $this->failures;
}
}
2 changes: 1 addition & 1 deletion src/StateHandlers/ClosedStateHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function onFailure(\Exception $exception)

$config = $this->breaker->getConfig();

if ($storage->getNumberOfFailures() >= $config->failureThreshold) {
if ($storage->getCounter()->numberOfFailures() >= $config->failureThreshold) {
$this->breaker->openCircuit();

throw CircuitOpenException::make($this->breaker->getName());
Expand Down
9 changes: 7 additions & 2 deletions src/StateHandlers/HalfOpenStateHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ class HalfOpenStateHandler extends StateHandler
*/
public function onSucess()
{
//@ToDo Close circuit after few successful calls
$this->breaker->closeCircuit();
$storage = $this->breaker->getStorage();

$storage->incrementSuccess();

if ($storage->getCounter()->numberOfSuccess() >= $this->breaker->getConfig()->consecutiveSuccess) {
$this->breaker->closeCircuit();
}
}

/**
Expand Down
7 changes: 4 additions & 3 deletions src/Storage/CircuitBreakerStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Stfn\CircuitBreaker\CircuitBreaker;
use Stfn\CircuitBreaker\CircuitState;
use Stfn\CircuitBreaker\Counter;

abstract class CircuitBreakerStorage
{
Expand Down Expand Up @@ -50,12 +51,12 @@ abstract public function incrementFailure(): void;
/**
* @return void
*/
abstract public function resetCounter(): void;
abstract public function incrementSuccess(): void;

/**
* @return int
* @return Counter
*/
abstract public function getNumberOfFailures(): int;
abstract public function getCounter(): Counter;

/**
* @return int
Expand Down
25 changes: 16 additions & 9 deletions src/Storage/InMemoryStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Stfn\CircuitBreaker\Storage;

use Stfn\CircuitBreaker\CircuitState;
use Stfn\CircuitBreaker\Counter;

class InMemoryStorage extends CircuitBreakerStorage
{
Expand All @@ -16,6 +17,11 @@ class InMemoryStorage extends CircuitBreakerStorage
*/
protected int $failCount = 0;

/**
* @var int
*/
protected int $successCount = 0;

/**
* @var int|null
*/
Expand All @@ -36,6 +42,9 @@ public function getState(): CircuitState
public function setState(CircuitState $state): void
{
$this->state = $state;

$this->failCount = 0;
$this->successCount = 0;
}

/**
Expand All @@ -49,17 +58,17 @@ public function incrementFailure(): void
/**
* @return void
*/
public function resetCounter(): void
public function incrementSuccess(): void
{
$this->failCount = 0;
$this->successCount++;
}

/**
* @return int
* @return Counter
*/
public function getNumberOfFailures(): int
public function getCounter(): Counter
{
return $this->failCount;
return new Counter($this->successCount, $this->failCount);
}

/**
Expand All @@ -75,19 +84,17 @@ public function openedAt(): int
*/
public function open(): void
{
$this->state = CircuitState::Open;
$this->setState(CircuitState::Open);

$this->openedAt = time();

$this->resetCounter();
}

/**
* @return void
*/
public function close(): void
{
$this->state = CircuitState::Closed;
$this->setState(CircuitState::Closed);

$this->openedAt = null;
}
Expand Down
34 changes: 20 additions & 14 deletions src/Storage/RedisStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

use Stfn\CircuitBreaker\CircuitBreaker;
use Stfn\CircuitBreaker\CircuitState;
use Stfn\CircuitBreaker\Counter;

class RedisStorage extends CircuitBreakerStorage
{
public const BASE_NAMESPACE = "stfn_php_circuit_breaker";
public const STATE_KEY = "state";
public const FAIL_COUNT_KEY = "fail_count";
public const SUCCESS_COUNT_KEY = "success_count";
public const OPENED_AT_KEY = "opened_at";

/**
Expand Down Expand Up @@ -61,6 +63,9 @@ public function getState(): CircuitState
public function setState(CircuitState $state): void
{
$this->redis->set($this->getNamespace(self::STATE_KEY), $state->value);

$this->redis->del($this->getNamespace(self::FAIL_COUNT_KEY));
$this->redis->del($this->getNamespace(self::SUCCESS_COUNT_KEY));
}

/**
Expand All @@ -75,13 +80,22 @@ public function incrementFailure(): void
);
}

/**
* @return void
* @throws \RedisException
*/
public function incrementSuccess(): void
{
$this->incrementOrCreate($this->getNamespace(self::SUCCESS_COUNT_KEY));
}

/**
* @param $key
* @param $ttl
* @return void
* @throws \RedisException
*/
protected function incrementOrCreate($key, $ttl)
protected function incrementOrCreate($key, $ttl = null)
{
if (! $this->redis->exists($key)) {
$this->redis->set($key, 0, $ttl);
Expand All @@ -91,21 +105,15 @@ protected function incrementOrCreate($key, $ttl)
}

/**
* @return void
* @return Counter
* @throws \RedisException
*/
public function resetCounter(): void
public function getCounter(): Counter
{
$this->redis->del($this->getNamespace(self::FAIL_COUNT_KEY));
}
$failures = (int) $this->redis->get($this->getNamespace(self::FAIL_COUNT_KEY));
$success = (int) $this->redis->get($this->getNamespace(self::SUCCESS_COUNT_KEY));

/**
* @return int
* @throws \RedisException
*/
public function getNumberOfFailures(): int
{
return (int) $this->redis->get($this->getNamespace(self::FAIL_COUNT_KEY));
return new Counter($success, $failures);
}

/**
Expand All @@ -126,8 +134,6 @@ public function open(): void
$this->setState(CircuitState::Open);

$this->redis->set($this->getNamespace(self::OPENED_AT_KEY), time());

$this->resetCounter();
}

/**
Expand Down
19 changes: 13 additions & 6 deletions tests/CircuitBreakerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function test_if_it_will_record_every_failure()
}
}

$this->assertEquals($tries, $breaker->getStorage()->getNumberOfFailures());
$this->assertEquals($tries, $breaker->getStorage()->getCounter()->numberOfFailures());
}

public function test_if_it_will_open_circuit_when_failure_threshold_is_reached()
Expand All @@ -91,18 +91,25 @@ public function test_if_it_will_open_circuit_when_failure_threshold_is_reached()
}

$this->assertTrue($breaker->isOpen());
$this->assertEquals(0, $breaker->getStorage()->getNumberOfFailures());
$this->assertEquals(0, $breaker->getStorage()->getCounter()->numberOfFailures());
}

public function test_if_it_will_close_circuit_after_success_call()
public function test_if_it_will_close_circuit_after_consecutive_success_calls()
{
$breaker = CircuitBreaker::for('test-service');
$breaker = CircuitBreaker::for('test-service')
->withOptions(['consecutive_success' => 3]);

$breaker->getStorage()->setState(CircuitState::HalfOpen);

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

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

$this->assertEquals(CircuitState::HalfOpen, $breaker->getStorage()->getState());

$breaker->call($success);

$this->assertEquals(CircuitState::Closed, $breaker->getStorage()->getState());
Expand Down Expand Up @@ -210,7 +217,7 @@ public function test_if_it_can_skip_some_exception()
throw $testException;
});

$this->assertEquals(0, $breaker->getStorage()->getNumberOfFailures());
$this->assertEquals(0, $breaker->getStorage()->getCounter()->numberOfFailures());
}

public function test_if_it_can_fail_even_without_exception()
Expand All @@ -232,7 +239,7 @@ public function test_if_it_can_fail_even_without_exception()
}

// Make sure that number of failures is reset to zero
$this->assertEquals(0, $breaker->getStorage()->getNumberOfFailures());
$this->assertEquals(0, $breaker->getStorage()->getCounter()->numberOfFailures());
$this->assertTrue($breaker->isOpen());
}

Expand Down
2 changes: 2 additions & 0 deletions tests/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ public function test_if_it_can_set_valid_config()
'failure_threshold' => 20,
'recovery_time' => 200,
'sample_duration' => 20,
'consecutive_success' => 3,
];

$config = Config::fromArray($setup);

$this->assertEquals($setup['failure_threshold'], $config->failureThreshold);
$this->assertEquals($setup['recovery_time'], $config->recoveryTime);
$this->assertEquals($setup['sample_duration'], $config->sampleDuration);
$this->assertEquals($setup['consecutive_success'], $config->consecutiveSuccess);

$this->assertEquals($setup, $config->toArray());
}
Expand Down
Loading

0 comments on commit d2be5fd

Please sign in to comment.