Appearance
<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /guides/CircuitBreaker.md for this page in Markdown format</div>
Circuit Breaker
Overview
The circuit breaker protects the application from cascading failures when external services (analytics, ad servers, CRMs) become unresponsive. Instead of repeatedly timing out against a down service, the breaker trips open and fails fast, preserving TTFB for end users.
[Closed] ─── (failures >= threshold) ──→ [Open]
↑ │
│ (cooldown elapsed)
│ ↓
│ (probe succeeds) [Half-Open]
└────────────────────────────────────────┤
recordSuccess() (only 1 probe allowed)
│
(probe fails)
│
(reopen with 2× cooldown)
→ [Open]States
| State | Behavior |
|---|---|
| Closed | Normal operation. Failures are counted. |
| Open | All calls short-circuit immediately for the duration of the cooldown. |
| Half-Open | Cooldown has elapsed. Exactly one probe request is allowed through (via atomic storeIfAbsent); all other concurrent callers are blocked until the probe resolves. |
On probe success the circuit closes. On probe failure it reopens with an exponentially increased cooldown, capped at maxCooldownSeconds.
Public API
php
use Advisable\CircuitBreaker\CircuitBreaker;
$cb = new CircuitBreaker(
cache: $l2Adapter, // CacheAdapterInterface (or null → no-op)
name: 'meta', // Unique identifier, used in cache keys
threshold: 3, // Failures before opening
cooldownSeconds: 30, // Initial cooldown duration
maxCooldownSeconds: 300, // Exponential backoff ceiling
cooldownMultiplier: 2.0, // Backoff multiplier per probe failure
stateTtlBuffer: 300, // Buffer added to max cooldown for cache TTL
logger: $logger // Optional PSR-3 logger
);
// Guard — check before making the external call
if ($cb->isOpen()) {
return; // fail fast
}
try {
$result = $httpClient->post($url, $payload);
$cb->recordSuccess(); // closes circuit, clears all state
} catch (\Throwable $e) {
$cb->recordFailure(); // increments counter, may open circuit
}
// Diagnostics
$status = $cb->getStatus();
// ['state' => 'open', 'failures' => 3, 'lastFailure' => 1710230400,
// 'secondsUntilOpen' => 18, 'currentCooldown' => 30]When $cache is null, all methods become no-ops — the breaker is effectively disabled and every call goes through.
Cache Keys and Storage
State is stored directly via CacheAdapterInterface (the L2 adapter), not through CachePool. The breaker needs adapter-level primitives like store() with native TTL and atomic storeIfAbsent() for probe locking, which are not available through the PSR-6 pool interface. When L2 is backed by Redis, breaker state is shared across all servers — one server tripping the breaker protects all others immediately.
| Key | Content | TTL |
|---|---|---|
cb_{name} | {failures, lastFailure, currentCooldown} | maxCooldownSeconds + stateTtlBuffer |
cb_{name}_probe | 1 (lock token) | Current cooldown duration |
The probe key is written atomically via storeIfAbsent() — only one worker wins the race to become the probe request during half-open state. The key auto-expires as a safety net if the probe worker crashes.
Exponential Backoff
Each time a probe fails in half-open state, the cooldown is multiplied:
Attempt 1: 30s (initial)
Attempt 2: 60s (30 × 2.0)
Attempt 3: 120s (60 × 2.0)
Attempt 4: 240s (120 × 2.0)
Attempt 5: 300s (capped at maxCooldownSeconds)A single success at any point resets the circuit to closed with all state cleared.
Configuration
Environment Variables
Each protected service has four tuning parameters:
bash
# Meta Conversions API (Facebook)
APP_CB_META_THRESHOLD=3
APP_CB_META_COOLDOWN_SECONDS=30
APP_CB_META_MAX_COOLDOWN_SECONDS=300
APP_CB_META_COOLDOWN_MULTIPLIER=2.0
APP_CB_META_STATE_TTL_BUFFER=300
# Matomo Analytics
APP_CB_MATOMO_THRESHOLD=3
APP_CB_MATOMO_COOLDOWN_SECONDS=30
APP_CB_MATOMO_MAX_COOLDOWN_SECONDS=300
APP_CB_MATOMO_COOLDOWN_MULTIPLIER=2.0
APP_CB_MATOMO_STATE_TTL_BUFFER=300
# Manago CRM
APP_CB_MANAGO_THRESHOLD=3
APP_CB_MANAGO_COOLDOWN_SECONDS=30
APP_CB_MANAGO_MAX_COOLDOWN_SECONDS=300
APP_CB_MANAGO_COOLDOWN_MULTIPLIER=2.0
APP_CB_MANAGO_STATE_TTL_BUFFER=300
# ProjectAgora Ad Server
APP_CB_PROJECT_AGORA_THRESHOLD=3
APP_CB_PROJECT_AGORA_COOLDOWN_SECONDS=30
APP_CB_PROJECT_AGORA_MAX_COOLDOWN_SECONDS=300
APP_CB_PROJECT_AGORA_COOLDOWN_MULTIPLIER=2.0
APP_CB_PROJECT_AGORA_STATE_TTL_BUFFER=300
# Advisable AI Recommendations
APP_CB_ADVISABLE_AI_THRESHOLD=3
APP_CB_ADVISABLE_AI_COOLDOWN_SECONDS=30
APP_CB_ADVISABLE_AI_MAX_COOLDOWN_SECONDS=300
APP_CB_ADVISABLE_AI_COOLDOWN_MULTIPLIER=2.0
APP_CB_ADVISABLE_AI_STATE_TTL_BUFFER=300Config file: application/config/circuit_breakers.php. All values have sensible defaults — no env vars are required.
Container Registration
application/config/container/circuit_breakers.php registers four DI services:
php
di()->get('circuit_breaker.meta');
di()->get('circuit_breaker.matomo');
di()->get('circuit_breaker.manago');
di()->get('circuit_breaker.project_agora');
di()->get('circuit_breaker.advisable_ai');All are backed by L2 cache via service('cache.l2')->nullOnInvalid() — if L2 is misconfigured, the breaker gracefully degrades to a no-op. Advisable AI lazy-loads its breaker from the container via di()->get() since it's a legacy CI library, not a DI-managed service.
Protected Services
All integrations follow the same guard-try-record pattern:
| Service | Class | Method | What it protects |
|---|---|---|---|
| Meta Conversions API | FacebookConversionService | dispatchEvent() | Facebook Pixel server-side events |
| Matomo | MatomoTracking | flush() | Bulk analytics tracking requests |
| Manago | Manago | doPostRequest() | Contact/recommendation/event API calls |
| ProjectAgora | ProjectAgoraHttpTrait | Async HTTP POST | Ad serving requests (Guzzle promises) |
| Advisable AI | AdvAdvisableAI | All external calls | User setup, recommendations, cart/bookmark/rating/purchase/view events |
All tracking calls are deferred to post-response execution via DeferredTaskRunner, so even without the circuit breaker, they don't block TTFB. The breaker prevents wasted time and log noise against a service that's known to be down.
Integration Pattern
php
// Typical service integration (simplified)
class SomeTrackingService
{
public function __construct(
private ?CircuitBreaker $circuitBreaker = null,
private ?LoggerInterface $logger = null,
) {}
public function send(array $data): void
{
if ($this->circuitBreaker?->isOpen()) {
return;
}
try {
$this->httpClient->post($this->endpoint, $data);
$this->circuitBreaker?->recordSuccess();
} catch (\Throwable $e) {
$this->circuitBreaker?->recordFailure();
$this->logger?->error('Tracking failed', ['error' => $e->getMessage()]);
}
}
}Testing
Unit tests: tests/Unit/CircuitBreaker/CircuitBreakerTest.php
bash
vendor/bin/phpunit tests/Unit/CircuitBreakerThe test suite covers closed/open/half-open transitions, atomic probe locking, exponential backoff with cap, null-cache no-op behavior, and state TTL computation.
File Locations
| File | Purpose |
|---|---|
src/CircuitBreaker/CircuitBreaker.php | Core implementation |
application/config/circuit_breakers.php | Per-service thresholds and cooldowns |
application/config/container/circuit_breakers.php | DI service registration |
src/MetaConversionsApi/Service/FacebookConversionService.php | Meta integration |
src/Analytics/MatomoTracking.php | Matomo integration |
src/Manago/Manago.php | Manago integration |
src/ProjectAgora/ProjectAgoraHttpTrait.php | ProjectAgora integration |
ecommercen/ai/libraries/AdvAdvisableAI.php | Advisable AI integration |
tests/Unit/CircuitBreaker/CircuitBreakerTest.php | Unit tests |