Skip to content

<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

StateBehavior
ClosedNormal operation. Failures are counted.
OpenAll calls short-circuit immediately for the duration of the cooldown.
Half-OpenCooldown 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.

KeyContentTTL
cb_{name}{failures, lastFailure, currentCooldown}maxCooldownSeconds + stateTtlBuffer
cb_{name}_probe1 (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=300

Config 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:

ServiceClassMethodWhat it protects
Meta Conversions APIFacebookConversionServicedispatchEvent()Facebook Pixel server-side events
MatomoMatomoTrackingflush()Bulk analytics tracking requests
ManagoManagodoPostRequest()Contact/recommendation/event API calls
ProjectAgoraProjectAgoraHttpTraitAsync HTTP POSTAd serving requests (Guzzle promises)
Advisable AIAdvAdvisableAIAll external callsUser 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/CircuitBreaker

The 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

FilePurpose
src/CircuitBreaker/CircuitBreaker.phpCore implementation
application/config/circuit_breakers.phpPer-service thresholds and cooldowns
application/config/container/circuit_breakers.phpDI service registration
src/MetaConversionsApi/Service/FacebookConversionService.phpMeta integration
src/Analytics/MatomoTracking.phpMatomo integration
src/Manago/Manago.phpManago integration
src/ProjectAgora/ProjectAgoraHttpTrait.phpProjectAgora integration
ecommercen/ai/libraries/AdvAdvisableAI.phpAdvisable AI integration
tests/Unit/CircuitBreaker/CircuitBreakerTest.phpUnit tests