Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GitAuto: [FEATURE] Add CircuitBreaker class #239

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
42 changes: 42 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# CircuitBreaker API Reference

## CircuitBreaker Class

### Methods

#### `__construct($cache, $failureThreshold = 5, $resetTimeout = 60)`
Dismissed Show dismissed Hide dismissed
Constructor to initialize the CircuitBreaker.

- **Parameters**:
- `$cache`: An instance of `MemoryCache` for state persistence.
- `$failureThreshold`: (Optional) Number of failures before opening the circuit. Default is 5.
- `$resetTimeout`: (Optional) Time in seconds to wait before transitioning from 'open' to 'half-open'. Default is 60.

#### `execute(callable $operation)`

Check warning

Code scanning / Markdownlint (reported by Codacy)

Expected: 1; Actual: 0; Below Warning documentation

Expected: 1; Actual: 0; Below
Executes the given operation if the circuit is closed or half-open.

- **Parameters**:
- `$operation`: A callable operation to execute.
- **Throws**:
- `CircuitBreakerOpenException` if the circuit is open and the reset timeout has not been reached.
- **Returns**: The result of the operation if successful.

### Exceptions

#### `CircuitBreakerOpenException`

Check warning

Code scanning / Markdownlint (reported by Codacy)

Expected: 1; Actual: 0; Below Warning documentation

Expected: 1; Actual: 0; Below
Exception thrown when an operation is attempted while the circuit is open.

## Example

```php
$cache = new MemoryCache();
$circuitBreaker = new CircuitBreaker($cache);

try {
$result = $circuitBreaker->execute(function() {
// Your operation here
});
} catch (CircuitBreakerOpenException $e) {
// Handle open circuit
}
```
34 changes: 34 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# CircuitBreaker Usage Guide

The `CircuitBreaker` class is designed to help manage failures in a system by implementing the Circuit Breaker design pattern. This guide provides an overview of how to use the `CircuitBreaker` class in your application.

## Installation

Ensure that you have the `MemoryCache` class available in your application, as it is required for state persistence.

## Basic Usage

```php
use App\CircuitBreaker;
use App\Exceptions\CircuitBreakerOpenException;

$cache = new MemoryCache();
$circuitBreaker = new CircuitBreaker($cache, 3, 120); // failureThreshold = 3, resetTimeout = 120 seconds

try {
$result = $circuitBreaker->execute(function() {
// Your operation here
return 'operation result';
});
echo $result;
} catch (CircuitBreakerOpenException $e) {
echo 'Circuit is open. Please try again later.';
} catch (Exception $e) {
echo 'Operation failed: ' . $e->getMessage();
}
```

## Configuration

- **failureThreshold**: The number of consecutive failures before the circuit opens.
- **resetTimeout**: The time in seconds to wait before transitioning from 'open' to 'half-open'.
83 changes: 83 additions & 0 deletions src/CircuitBreaker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace App;

use App\Exceptions\CircuitBreakerOpenException;

class CircuitBreaker
{
private $state;
private $failureCount;
private $lastFailureTime;
private $failureThreshold;
private $resetTimeout;
private $cache;

public function __construct($cache, $failureThreshold = 5, $resetTimeout = 60)
{
$this->cache = $cache;
$this->failureThreshold = $failureThreshold;
$this->resetTimeout = $resetTimeout;
$this->loadState();
}

private function loadState()
{
$state = $this->cache->get('circuit_state');
$this->state = $state['state'] ?? 'closed';
$this->failureCount = $state['failureCount'] ?? 0;
$this->lastFailureTime = $state['lastFailureTime'] ?? null;
}

private function saveState()
{
$this->cache->set('circuit_state', [
'state' => $this->state,
'failureCount' => $this->failureCount,
'lastFailureTime' => $this->lastFailureTime,
]);
}

public function execute(callable $operation)
{
if ($this->state === 'open' && !$this->isTimeoutReached()) {
throw new CircuitBreakerOpenException('Circuit is open. Please try again later.');
}

if ($this->state === 'open' && $this->isTimeoutReached()) {
$this->state = 'half-open';
}

try {
$result = $operation();
$this->reset();
return $result;
} catch (\Exception $e) {
$this->handleFailure();
throw $e;
} finally {
$this->saveState();
}
}

private function handleFailure()
{
$this->failureCount++;
$this->lastFailureTime = time();
if ($this->failureCount >= $this->failureThreshold) {
$this->state = 'open';
}
}

private function isTimeoutReached()
{
return (time() - $this->lastFailureTime) > $this->resetTimeout;
}

private function reset()
{
$this->state = 'closed';
$this->failureCount = 0;
$this->lastFailureTime = null;
}
}
9 changes: 9 additions & 0 deletions src/Exceptions/CircuitBreakerOpenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Exceptions;

use Exception;

class CircuitBreakerOpenException extends Exception
{
}
82 changes: 82 additions & 0 deletions tests/CircuitBreakerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

use PHPUnit\Framework\TestCase;
use App\CircuitBreaker;
use App\Exceptions\CircuitBreakerOpenException;

class CircuitBreakerTest extends TestCase
{
private $cacheMock;
private $circuitBreaker;

protected function setUp(): void
{
$this->cacheMock = $this->createMock(MemoryCache::class);
$this->circuitBreaker = new CircuitBreaker($this->cacheMock);
}

public function testInitialization()
{
$this->cacheMock->method('get')->willReturn(null);
$circuitBreaker = new CircuitBreaker($this->cacheMock);
$this->assertEquals('closed', $circuitBreaker->getState());
}

public function testStateTransitionsToOpen()
{
$this->cacheMock->method('get')->willReturn(null);
$circuitBreaker = new CircuitBreaker($this->cacheMock, 1);

try {
$circuitBreaker->execute(function () {
throw new Exception('Failure');
});
} catch (Exception $e) {
// Ignore
}

$this->assertEquals('open', $circuitBreaker->getState());
}

public function testExecutionThrowsExceptionWhenOpen()
{
$this->cacheMock->method('get')->willReturn([
'state' => 'open',
'failureCount' => 1,
'lastFailureTime' => time()
]);

$this->expectException(CircuitBreakerOpenException::class);

$this->circuitBreaker->execute(function () {
return 'success';
});
}

public function testExecutionResetsAfterSuccess()
{
$this->cacheMock->method('get')->willReturn([
'state' => 'half-open',
'failureCount' => 1,
'lastFailureTime' => time() - 100
]);

$this->circuitBreaker->execute(function () {
return 'success';
});

$this->assertEquals('closed', $this->circuitBreaker->getState());
}

public function testPersistenceBetweenExecutions()
{
$this->cacheMock->method('get')->willReturn([
'state' => 'open',
'failureCount' => 3,
'lastFailureTime' => time()
]);

$circuitBreaker = new CircuitBreaker($this->cacheMock);
$this->assertEquals('open', $circuitBreaker->getState());
}
}
Loading