Я щойно презентував Laravel Fuse на сцені Laracon India 2026. Це пакет, який я створив, щоб вирішити знайому проблему: робочі черги зупиняються, коли падає зовнішній сервіс.
Проблема
Уявіть: 23:00, у Stripe — збій. Ваші воркери про це не знають. Вони продовжують намагатися списувати гроші, і кожна задача чекає 30 секунд на таймаут, перш ніж впасти — а потім повторити спробу. І знову чекати.
Якщо в черзі 10 000 платіжних задач і кожна чекає по 30 секунд — на очищення черги піде понад 25 годин, хоча всі запити гарантовано падають.
Fuse вирішує це через реалізацію патерну circuit breaker. Після заданої кількості невдач він припиняє робити запити. Задачі відпадають за мілісекунди замість очікування таймаутів і автоматично повертаються в чергу з відстрочкою для повторної обробки. Коли сервіс відновлюється, Fuse це виявляє і відновлює роботу.
Як працює Circuit Breaker
Circuit breaker має три стани:
CLOSED — звичайна робота. Всі запити йдуть до зовнішнього сервісу. У фоні Fuse відстежує успіхи й помилки у хвилинних бакетах, які автоматично звільняються.
OPEN — режим захисту. Якщо відсоток помилок перевищує поріг, ланцюг розмикається. Задачі одразу падають без виклику API і без 30‑секундних таймаутів; вони повертаються в чергу з затримкою. Черга продовжує рух.
HALF-OPEN — тест відновлення. Після таймауту (налаштовується для кожного сервісу) Fuse пропускає одну пробну заявку. Якщо вона успішна — ланцюг закривається; якщо ні — відбувається повторне відкриття і очікування.
Інсталяція
composer require harris21/laravel-fuse
Опублікуйте конфіг:
php artisan vendor:publish --tag=fuse-config
Базове використання
Додайте middleware до задачі, яка викликає зовнішній сервіс:
use Harris21\Fuse\Middleware\CircuitBreakerMiddleware;
class ChargeCustomer implements ShouldQueue
{
public $tries = 0;
public $maxExceptions = 3;
public function middleware(): array
{
return [new CircuitBreakerMiddleware('stripe')];
}
public function handle(): void
{
Stripe::charges()->create([
'amount' => $this->amount,
'currency' => 'usd',
'customer' => $this->customerId,
]);
}
}
Налаштування $tries = 0 дозволяє необмежену кількість release (у термінах Laravel released jobs не рахуються як “retries”), а $maxExceptions = 3 обмежує фактичні помилки. Саму задачу змінювати не потрібно — Fuse обгортає її.
Конфігурація
Опублікований конфіг дозволяє встановити значення за замовчуванням і для кожного сервісу окремо:
// config/fuse.php
return [
'enabled' => env('FUSE_ENABLED', true),
'default_threshold' => 50,
'default_timeout' => 60,
'default_min_requests' => 10,
'services' => [
'stripe' => [
'threshold' => 50,
'timeout' => 30,
'min_requests' => 5,
],
'mailgun' => [
'threshold' => 60,
'timeout' => 120,
'min_requests' => 10,
],
],
];
threshold — відсоток помилок, при якому ланцюг відкривається. timeout — секундах до наступної перевірки відновлення. min_requests — мінімальна кількість запитів, щоб уникнути тригеру на малих вибірках.
Інтелектуальна класифікація помилок
Не кожна помилка означає, що сервіс впав. Наприклад, 429 — це rate limit, сервіс працює, просто занадто багато запитів. Те ж стосується помилок аутентифікації.
Fuse рахує як невдачі лише ті випадки, що вказують на проблему сервісу:
- 500, 502, 503 — серверні помилки рахується як невдачі
- таймаути з’єднання і відмовлені з’єднання рахується як невдачі
- 429 — rate limits не враховуються
- 401 і 403 — помилки аутентифікації не враховуються
Це зменшує хибні спрацювання. Ланцюг не відкриється просто через неактивний або неправильний API‑ключ.
Підтримка пікових годин
У робочі години ви можете бути толерантніші до помилок, щоб зберегти пропускну здатність, а поза ними — швидше захиститись. Fuse підтримує це:
'stripe' => [
'threshold' => 40,
'peak_hours_threshold' => 60,
'peak_hours_start' => 9,
'peak_hours_end' => 17,
],
Між 9:00 і 17:00 використовується поріг 60%, поза цими годинами — 40%. Так ви коригуєте баланс між захистом і throughput залежно від важливості транзакцій.
Події
Fuse відсилає Laravel‑події при кожній зміні стану:
use Harris21\Fuse\Events\CircuitBreakerOpened;
use Harris21\Fuse\Events\CircuitBreakerHalfOpen;
use Harris21\Fuse\Events\CircuitBreakerClosed;
Їх можна слухати для алертів:
class AlertOnCircuitOpen
{
public function handle(CircuitBreakerOpened $event): void
{
Log::critical("Circuit opened for {$event->service}", [
'failure_rate' => $event->failureRate,
'attempts' => $event->attempts,
'failures' => $event->failures,
]);
// Send to Slack, page on-call, etc.
}
}
Подія CircuitBreakerOpened містить ім’я сервісу, поточний відсоток помилок, число спроб і кількість невдач — достатньо для дебагу та алертингу.
Пряме використання
Circuit breaker можна використовувати і поза чергами:
use Harris21\Fuse\CircuitBreaker;
$breaker = new CircuitBreaker('stripe');
if (!$breaker->isOpen()) {
try {
$result = Stripe::charges()->create([...]);
$breaker->recordSuccess();
return $result;
} catch (Exception $e) {
$breaker->recordFailure($e);
throw $e;
}
} else {
return $this->fallbackResponse();
}
Можна також перевіряти стан і скинути вручну:
$breaker->isClosed();
$breaker->isOpen();
$breaker->isHalfOpen();
$breaker->getStats();
$breaker->reset();
Запобігання thundering herd
Коли ланцюг переходить у HALF-OPEN, небажано, щоб 50 воркерів одночасно робили пробні запити. Fuse використовує Cache::lock(), щоб лише один воркер тестував сервіс; інші падають швидко, поки проба не завершиться.
Вимоги
- PHP 8.3+
- Laravel 11+
- Будь‑який Laravel cache driver (для продакшену рекомендований Redis)
Пакет не має зовнішніх залежностей — він використовує вбудований cache і механізм lock у Laravel.
Посилання
- GitHub: harris21/laravel-fuse
Патерн circuit breaker походить із книжки Michael Nygard "Release It!" і став відомим завдяки Martin Fowler. Fuse приніс його в Laravel з нативною інтеграцією та мінімальною конфігурацією для початку роботи.