Circuit Breaker¶
Nene\Kit\CircuitBreaker is a DB-backed resilience primitive that prevents cascading failures when an external service (payment gateway, third-party API, etc.) becomes unavailable.
Problem¶
Without protection, a slow or failing downstream service causes every request to block and fail at the network layer, consuming threads or file handles until the application itself degrades. A circuit breaker short-circuits those calls once failures accumulate, returning a fast error immediately and periodically probing for recovery.
States¶
failure >= threshold
CLOSED ─────────────────────► OPEN
▲ │
│ success │ cooldown elapsed
│ ▼
└──────────────────────── HALF-OPEN
success │
│ failure
▼
OPEN (timer resets)
| State | Behaviour |
|---|---|
closed |
Normal operation. Every call is allowed. Failures are counted. |
open |
All calls are rejected immediately. No network traffic is generated. After openSeconds seconds the circuit transitions to half_open. |
half_open |
One probe call is allowed. A success transitions back to closed and resets the counter. A failure transitions back to open and resets the timer. |
Schema¶
Run this one-time migration before the first deployment that uses CircuitBreaker:
CREATE TABLE circuit_breaker_states (
name VARCHAR(100) NOT NULL PRIMARY KEY,
state VARCHAR(20) NOT NULL DEFAULT 'closed',
failures INT NOT NULL DEFAULT 0,
opened_at DATETIME NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
API¶
| Method | Signature | Description |
|---|---|---|
__construct |
(string $name, ?PDO $db, int $failureThreshold, int $openSeconds, string $table) |
$name is the logical service name (primary key in the table). Defaults: $db = PdoConnection::getInstance(), $failureThreshold = 5, $openSeconds = 60, $table = circuit_breaker_states. |
isAvailable |
(?int $now = null): bool |
Returns true when a call should be attempted. Returns false when the circuit is OPEN and the cooldown has not elapsed. Side-effect: if the cooldown has elapsed, the circuit is transitioned to HALF-OPEN. |
recordSuccess |
(): void |
Closes the circuit and resets the failure counter. Call after every successful downstream response. |
recordFailure |
(?int $now = null): void |
Increments the failure counter. Opens the circuit when the counter reaches $failureThreshold. The $now parameter is an injectable Unix timestamp (useful in tests). |
getState |
(): string |
Returns the current state constant: CircuitBreaker::STATE_CLOSED, STATE_OPEN, or STATE_HALF_OPEN. |
failureCount |
(): int |
Returns the current failure count. Returns 0 if no record exists. |
reset |
(): void |
Manually closes the circuit and resets the counter. Use from admin endpoints. |
State constants¶
CircuitBreaker::STATE_CLOSED // 'closed'
CircuitBreaker::STATE_OPEN // 'open'
CircuitBreaker::STATE_HALF_OPEN // 'half_open'
Usage Pattern¶
Wrap every external call with the same try/catch pattern:
use Nene\Kit\CircuitBreaker;
$cb = new CircuitBreaker('payment-api');
if (!$cb->isAvailable()) {
// Respond immediately without touching the network.
JsonResponder::outputError('CIRCUIT-OPEN', 'Payment API is temporarily unavailable.');
}
try {
$response = $httpClient->post('/charge', $payload);
$cb->recordSuccess();
return $response;
} catch (\Exception $e) {
$cb->recordFailure();
throw $e;
}
The CIRCUIT-OPEN error code maps to HTTP 503 (see config/error_codes.php).
Configuration Parameters¶
| Parameter | Default | Notes |
|---|---|---|
$failureThreshold |
5 |
Number of consecutive failures required to open the circuit. |
$openSeconds |
60 |
Seconds the circuit stays OPEN before transitioning to HALF-OPEN. |
Adjust these per service. A slow but essential service warrants a higher threshold and a shorter cooldown; a flaky low-priority service may deserve a lower threshold and a longer cooldown.
Multiple Circuits¶
Each logical service should have its own named circuit. Names are arbitrary strings stored as the primary key in circuit_breaker_states:
$paymentCb = new CircuitBreaker('payment-api');
$shippingCb = new CircuitBreaker('shipping-api');
$smsCb = new CircuitBreaker('sms-gateway');
Manual Reset (Admin Endpoint Pattern)¶
Expose a protected admin endpoint to clear a circuit manually after a known incident:
// AdminController — POST /admin/circuit-breaker/reset
public function resetAction(): void
{
$name = $this->request->post('name') ?? '';
(new CircuitBreaker($name))->reset();
JsonResponder::output(['reset' => $name]);
}
Guard the endpoint with authentication and audit-log the action.
Related¶
class/kit/CircuitBreaker.php— implementation.tests/Unit/Kit/CircuitBreakerTest.php— unit tests (SQLite:memory:).config/error_codes.php—CIRCUIT-OPENerror code (HTTP 503).- NENE2 FT137 — Python-side circuit breaker trial that informed this design.
docs/field-trials/2026-05-field-trial-43.md— trial report.