Circuit Breaker

Prevent cascading failures with automatic handler protection

Overview

The Circuit Breaker pattern prevents cascading failures by automatically disabling handlers that fail repeatedly. When a handler fails too many times, its circuit “opens” and the handler is temporarily skipped.

How It Works

     ┌──────────────────────────────────────────────────────┐
     │                   Circuit States                      │
     │                                                       │
     │   ┌─────────┐      failures       ┌─────────┐        │
     │   │ CLOSED  │ ──────────────────▶ │  OPEN   │        │
     │   │ (normal)│                      │ (skip)  │        │
     │   └────┬────┘                      └────┬────┘        │
     │        │                                │             │
     │        │ success                        │ timeout     │
     │        │                                │             │
     │        │         ┌───────────┐          │             │
     │        └─────────│ HALF-OPEN │◀─────────┘             │
     │                  │  (test)   │                        │
     │                  └───────────┘                        │
     └──────────────────────────────────────────────────────┘

States

State Description Behavior
Closed Normal operation Handler executes normally
Open Handler disabled Handler is skipped
Half-Open Testing recovery Single request allowed to test

Configuration

// config/extensions.php
'circuit_breaker' => [
    'enabled' => true,
    'threshold' => 5,        // Failures before opening
    'timeout' => 60,         // Seconds before half-open
    'half_open_max' => 3,    // Successes to close
    'store' => 'cache',      // State storage
],

Basic Usage

The circuit breaker works automatically when enabled:

// If SendEmailHandler fails 5 times, it's automatically skipped
Extensions::dispatch(new UserCreated($user));

Checking Circuit Status

use Esegments\LaravelExtensions\Facades\Extensions;

// Check specific handler status
$status = Extensions::circuitBreaker()->status(SendEmailHandler::class);

// CircuitState enum: Closed, Open, HalfOpen
if ($status === CircuitState::Open) {
    Log::warning('Email handler circuit is open');
}

// Get failure count
$failures = Extensions::circuitBreaker()->failureCount(SendEmailHandler::class);

CLI Commands

# View circuit breaker status
php artisan extension:stats

# Output:
# Circuit Breaker Status
# ┌─────────────────────┬───────────┬──────────┐
# │ Handler             │ State     │ Failures │
# ├─────────────────────┼───────────┼──────────┤
# │ SendEmailHandler    │ Open      │ 5        │
# │ NotifySlackHandler  │ Half-Open │ 3        │
# └─────────────────────┴───────────┴──────────┘

Manual Control

Reset a Circuit

Extensions::circuitBreaker()->reset(SendEmailHandler::class);

Force Open

Extensions::circuitBreaker()->open(SendEmailHandler::class);

Disable for Handler

Extensions::circuitBreaker()->exclude(SendEmailHandler::class);

Storage

Circuit state is stored in cache by default:

'circuit_breaker' => [
    'store' => 'cache',  // Uses Laravel cache
],

For multi-server deployments, use Redis:

'circuit_breaker' => [
    'store' => 'redis',
],

Events

The circuit breaker fires events:

// Listen for circuit state changes
Event::listen(CircuitOpened::class, function ($event) {
    Log::alert("Circuit opened for {$event->handler}");
    // Notify on-call team
});

Event::listen(CircuitClosed::class, function ($event) {
    Log::info("Circuit recovered for {$event->handler}");
});

Graceful Degradation

When a circuit is open, consider fallback behavior:

class SendEmailHandler implements ExtensionHandlerContract
{
    public function handle(ExtensionPointContract $extension): void
    {
        try {
            // Primary: Send via SMTP
            Mail::send(...);
        } catch (Exception $e) {
            // Fallback: Queue for later
            SendEmailJob::dispatch($extension->user)->delay(now()->addMinutes(5));
            throw $e; // Re-throw to trigger circuit breaker
        }
    }
}

Best Practices

1. Set Appropriate Thresholds

// For critical handlers, higher threshold
'threshold' => 10,

// For non-critical handlers, lower threshold
'threshold' => 3,

2. Monitor Circuit Health

// In a scheduled command
$breaker = Extensions::circuitBreaker();
$openCircuits = collect($breaker->getAllStatuses())
    ->filter(fn ($status) => $status === CircuitState::Open);

if ($openCircuits->isNotEmpty()) {
    // Alert operations team
}

3. Test Recovery

// Periodically test half-open circuits
$schedule->command('extension:health-check')
    ->everyFiveMinutes();

Disabling Circuit Breaker

For development or specific handlers:

// Globally disable
'circuit_breaker' => [
    'enabled' => false,
],

// Or per-dispatch
Extensions::withoutCircuitBreaker()->dispatch($extension);