|
1 | | -[](https://travis-ci.com/gabrielanhaia/php-circuit-breaker) |
2 | | - |
| 1 | + |
3 | 2 |  |
4 | | - |
5 | 3 |
|
6 | 4 | # PHP Circuit Breaker |
7 | 5 |
|
8 | | -## Description |
| 6 | +Resiliency pattern for PHP microservices. This library implements a circuit breaker using Redis for state tracking. It protects your services from cascading failures by stopping calls to unhealthy dependencies and gradually allowing traffic as they recover. |
9 | 7 |
|
10 | | -PHP Circuit Breaker was developed based on the book "Release It!: Design and Deploy Production-Ready Software (Pragmatic Programmers)", written by Michael T. Nygard. |
11 | | -In this book, Michael popularized the Circuit Breaker. |
| 8 | +Learn more about circuit breakers: https://martinfowler.com/bliki/CircuitBreaker.html |
12 | 9 |
|
13 | | -When we work with microservices, it is sometimes common to call these systems, and they are not available, which ends up causing problems in our application. To prevent any problem on our side, and guarantee that a service will not be called loads of times, we should use a Circuit Breaker. |
| 10 | +## Features |
| 11 | +- Simple API: `canPass`, `succeed`, `failed` |
| 12 | +- Redis-backed state with configurable windows/timeouts |
| 13 | +- Pluggable alert hook to notify when circuits open |
| 14 | +- PHP 8.1+ native enum for states in v2 |
14 | 15 |
|
15 | | -You can find more information about Circuit Breakers [here](https://martinfowler.com/bliki/CircuitBreaker.html). |
| 16 | +## Versions |
| 17 | +- 2.x (current): PHP 8.1+, native enums, PHPUnit 10, GitHub Actions |
| 18 | +- 1.x (legacy): PHP 7.4+/8.0+, uses `eloquent/enumeration` and Travis CI |
16 | 19 |
|
17 | | -## Requirements |
18 | | - |
19 | | -- PHP 8.1+ (library runtime also supports PHP 7.4, but development tools target PHP 8.1+) |
20 | | -- Redis |
21 | | -- Redis PHP extension enabled |
22 | | -- Composer |
23 | | - |
24 | | -___ |
25 | | - |
26 | | -## Installation |
27 | | - |
28 | | -You can install **PHP Circuit Breaker** by composer running: |
29 | | -```# composer require gabrielanhaia/php-circuit-breaker``` |
| 20 | +See CHANGELOG for details and migration notes. |
30 | 21 |
|
| 22 | +## Requirements |
| 23 | +- PHP 8.1+ |
| 24 | +- Redis server |
| 25 | +- `ext-redis` PHP extension |
31 | 26 |
|
32 | | -___ |
33 | | - |
34 | | -## How do I use it? |
35 | | - |
36 | | -I strongly recommend that you use a service container (dependency injection container) to deal with the objects and their dependencies, so you will be able to use it efficiently (It will not be necessary to create instances everywhere). |
37 | | - |
38 | | -1. The first thing you can do is to define the *Settings*: |
| 27 | +## Install |
| 28 | +- v2 (recommended): `composer require gabrielanhaia/php-circuit-breaker:^2.0` |
| 29 | +- v1 (legacy): `composer require gabrielanhaia/php-circuit-breaker:^1.0` |
39 | 30 |
|
| 31 | +## Quick Start |
40 | 32 | ```php |
| 33 | +use GabrielAnhaia\PhpCircuitBreaker\Adapter\Redis\RedisCircuitBreaker; |
| 34 | +use GabrielAnhaia\PhpCircuitBreaker\CircuitBreaker; |
| 35 | + |
41 | 36 | $settings = [ |
42 | | - 'exceptions_on' => false, // Define if exceptions will be thrown when the circuit is open. |
43 | | - 'time_window' => 20, // Time window in which errors accumulate (Are being accounted for in total). |
44 | | - 'time_out_open' => 30, // Time window that the circuit will be opened (If opened). |
45 | | - 'time_out_half_open' => 20, // Time out that the circuit will be half-open. |
46 | | - 'total_failures' => 5 // Number of failures necessary to open the circuit. |
| 37 | + 'exceptions_on' => false, |
| 38 | + 'time_window' => 20, |
| 39 | + 'time_out_open' => 30, |
| 40 | + 'time_out_half_open' => 20, |
| 41 | + 'total_failures' => 5, |
47 | 42 | ]; |
48 | | -``` |
49 | | - |
50 | | -*Note: It is not necessary to define these settings (they are the default values), they will be defined automatically.* |
51 | | - |
52 | | -2. Instantiating a driver (Only Redis driver is available at the moment) and Redis client: |
53 | | - |
54 | | -```php |
55 | | -$redis = new \Redis; |
56 | | -$redis->connect('localhost'); |
57 | | -$redisCircuitBreakerDriver = new GabrielAnhaia\PhpCircuitBreaker\Adapter\Redis\RedisCircuitBreaker($redis); |
58 | | - |
59 | | -``` |
60 | | - |
61 | | -3. Instantiating the **PHP Circuit Breaker** class: |
62 | 43 |
|
63 | | -```php |
64 | | -$circuitBreaker = new GabrielAnhaia\PhpCircuitBreaker\CircuitBreaker($redisCircuitBreakerDriver, $settings) |
65 | | -``` |
66 | | -*Note: The second parameter is optional.* |
| 44 | +$redis = new \Redis(); |
| 45 | +$redis->connect('127.0.0.1', 6379); |
67 | 46 |
|
68 | | -4. Validating if the circuit is open: |
| 47 | +$driver = new RedisCircuitBreaker($redis); |
| 48 | +$cb = new CircuitBreaker($driver, $settings); |
69 | 49 |
|
70 | | -```php |
71 | | -if ($circuitBreaker->canPass($serviceName) !== true) { |
| 50 | +$service = 'PAYMENTS_API'; |
| 51 | +if (!$cb->canPass($service)) { |
| 52 | + // Short-circuit |
72 | 53 | return; |
73 | 54 | } |
74 | | -``` |
75 | | - |
76 | | -You can use the function **canPass** in any way you want. It will always return *true* when the Circuit is **CLOSED** or **HALF_OPEN**. |
77 | | -After that, you should call your service, and depending on the response, you can call the following methods to update the circuit control variables. |
78 | 55 |
|
79 | | -If Success: |
80 | | -```php |
81 | | -$circuitBreaker->succeed($serviceName); |
82 | | -``` |
83 | | - |
84 | | -If failure: |
85 | | -```php |
86 | | -$circuitBreaker->failed($serviceName); |
| 56 | +try { |
| 57 | + // Call dependency... |
| 58 | + $cb->succeed($service); |
| 59 | +} catch (\Throwable $e) { |
| 60 | + $cb->failed($service); |
| 61 | +} |
87 | 62 | ``` |
88 | 63 |
|
89 | | -With these three simple methods, you can control the flow of your application in execution time. |
90 | | - |
91 | | -### PHP 8.1+ Native Enum (Optional) |
92 | | - |
93 | | -For projects on PHP 8.1 or newer, a native enum `GabrielAnhaia\PhpCircuitBreaker\CircuitStateEnum` is available. It mirrors the three states (`OPEN`, `CLOSED`, `HALF_OPEN`) and can be used in your own code to represent circuit state values. The existing `GabrielAnhaia\PhpCircuitBreaker\CircuitState` remains for backward compatibility and will be considered for replacement in a future major version. |
94 | | - |
95 | | - |
96 | | -___ |
| 64 | +## Configuration |
| 65 | +- `exceptions_on` (bool): throw when circuit is open (default: false) |
| 66 | +- `time_window` (int): seconds to track failures (default: 20) |
| 67 | +- `time_out_open` (int): seconds to keep circuit open (default: 30) |
| 68 | +- `time_out_half_open` (int): additional seconds before half-open closes (default: 20) |
| 69 | +- `total_failures` (int): failures within window to open (default: 5) |
97 | 70 |
|
98 | | -## Recap |
| 71 | +## Circuit State |
| 72 | +In v2+, states are represented with a native enum: `GabrielAnhaia\PhpCircuitBreaker\CircuitStateEnum` with cases `OPEN`, `CLOSED`, `HALF_OPEN`. |
| 73 | +You don’t normally need to consume this directly unless you’re writing custom adapters. |
99 | 74 |
|
100 | | -Let's say that you are using the following settings: |
| 75 | +## Alerts |
| 76 | +Implement `GabrielAnhaia\PhpCircuitBreaker\Contract\Alert` and pass it to `CircuitBreaker` to receive callbacks when a circuit opens. |
101 | 77 |
|
102 | 78 | ```php |
103 | | -$settings = [ |
104 | | - 'exceptions_on' => false, // Define if exceptions will be thrown when the circuit is open. |
105 | | - 'time_window' => 20, // Time window in which errors accumulate (Are being accounted for in total). |
106 | | - 'time_out_open' => 30, // Time window that the circuit will be opened (If opened). |
107 | | - 'time_out_half_open' => 60, // Time out that the circuit will be half-open. |
108 | | - 'total_failures' => 5 // Number of failures necessary to open the circuit. |
109 | | -]; |
| 79 | +use GabrielAnhaia\PhpCircuitBreaker\Contract\Alert; |
| 80 | + |
| 81 | +class LoggerAlert implements Alert { |
| 82 | + public function emmitOpenCircuit(string $serviceName) |
| 83 | + { |
| 84 | + error_log("Circuit opened: {$serviceName}"); |
| 85 | + } |
| 86 | +} |
110 | 87 | ``` |
111 | 88 |
|
112 | | -One of your services is a Payment Gateway, and you try to call it in an interval of each 2 seconds for some reason. |
113 | | -The first time you call the Gateway, it responds with a 200 (HTTP status code), and after you call the method "succeed" with a service identifier (You can create one for each service). |
| 89 | +## Redis Keys |
| 90 | +Keys are namespaced per service: |
| 91 | +- `circuit_breaker:{SERVICE}:open` |
| 92 | +- `circuit_breaker:{SERVICE}:half_open` |
| 93 | +- `circuit_breaker:{SERVICE}:total_failures:*` |
114 | 94 |
|
115 | | -On the second, third, fourth, fifth, and sixth call, the Gateway is unavailable, so you call the method "failed" again. |
| 95 | +## Migration (1.x → 2.x) |
| 96 | +Breaking changes: |
| 97 | +- Replace `CircuitState::OPEN()` style calls with native enum values if referenced in your code, e.g. `CircuitStateEnum::OPEN`. |
| 98 | +- `CircuitBreakerAdapter::getState(string): CircuitStateEnum` now returns the native enum. |
116 | 99 |
|
117 | | -The total of failers was 5, now the next time you call the method "canPass" it will return "false" and the service will not be called again. |
118 | | -At this moment the circuit is open, it will stay "OPEN" for 30 seconds (time_out_open), and then it will change the state to "HALF_OPEN" at this moment you can try to call the service again, and if it fails it will be "OPEN" for more 30 seconds. |
| 100 | +Otherwise, `CircuitBreaker` public API is unchanged. |
119 | 101 |
|
120 | | -What happens if the first four attempts fail and the fifth is succeeded? |
121 | | -Then, the counter will be reset. |
| 102 | +## Development |
| 103 | +- Run tests: `vendor/bin/phpunit --configuration phpunit.xml` |
| 104 | +- GitHub Actions runs on PHP 8.1–8.4 |
122 | 105 |
|
123 | | -What is the setting "time_window" for? |
124 | | -Each failure is stored on Redis and has an expiration date. |
125 | | -If the first failure happened exaclty at 12:00:10 and the "time_window" is 30 seconds, so, after 12:00:40 this failure will not be counted in the total of failures considered to open the circuit. |
126 | | -In short, to open the circuit, you must have X (total_failures) in an interval of Y (time_window) seconds. |
| 106 | +## License |
| 107 | +MIT |
127 | 108 |
|
| 109 | +--- |
128 | 110 |
|
129 | | -___ |
| 111 | +Created by: Gabriel Anhaia — https://www.linkedin.com/in/gabrielanhaia |
130 | 112 |
|
131 | | -Created by: **Gabriel Anhaia** - [https://www.linkedin.com/in/gabrielanhaia](https://www.linkedin.com/in/gabrielanhaia) |
0 commit comments