Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Що таке патерн Circuit Breaker і як застосувати його до інтеграцій у Laravel?

Коли сторонній сервіс лежить, повтори й тайм-аути не допомагають, а шкодять: кожен запит чекає повний тайм-аут, воркери й процеси PHP зайняті очікуванням, черга росте, а сервіс, що намагається піднятися, отримує лавину запитів.

Circuit Breaker («запобіжник») відстежує помилки й після порогу перестає викликати сервіс на певний час:

Closed (норма)  -- N помилок поспіль -->  Open (виклики не йдуть, одразу помилка)
     ^                                          |
     |                                     минув час
     |                                          v
     +----- успіх ----  Half-open (пропустити пробний запит)  ---- помилка ---> Open

Що це дає:

  • швидка відмова замість 30-секундного очікування: користувач одразу бачить запасний варіант, воркери вільні;
  • сервісу дають відновитися без лавини повторів;
  • запасна поведінка: закешовані дані, відкладена обробка, повідомлення «сервіс тимчасово недоступний».

У Laravel для фонових задач роль запобіжника виконує middleware ThrottlesExceptions:

public function middleware(): array
{
    return [
        (new ThrottlesExceptions(maxAttempts: 10, decaySeconds: 5 * 60))
            ->by('payment-provider')                                  // спільно для всіх джоб провайдера
            ->when(fn (Throwable $e) => $e instanceof ConnectionException
                || ($e instanceof RequestException && $e->response->serverError())),
    ];
}

Після 10 помилок джоби з цим ключем не виконуються 5 хвилин, а повертаються в чергу. З retryUntil() вони не губляться.

Для синхронних викликів простий запобіжник на кеші:

final class CircuitBreaker
{
    public function __construct(private string $service, private int $threshold = 5, private int $cooldown = 60) {}

    public function call(callable $callback, callable $fallback): mixed
    {
        if (Cache::has("circuit:{$this->service}:open")) {
            return $fallback();
        }

        try {
            $result = $callback();
            Cache::forget("circuit:{$this->service}:failures");
            return $result;
        } catch (ConnectionException|RequestException $e) {
            if (Cache::increment("circuit:{$this->service}:failures") >= $this->threshold) {
                Cache::put("circuit:{$this->service}:open", true, $this->cooldown);
            }
            return $fallback();
        }
    }
}

Що важливо:

  • рахувати лише «системні» помилки (мережа, 5xx, тайм-аути), а не 4xx від ваших же некоректних запитів;
  • стан спільний для всіх процесів - у Redis, а не в пам'яті одного воркера;
  • сповіщення: перехід у стан Open - подія для моніторингу й чергового;
  • короткі тайм-аути - запобіжник не замінює їх, а доповнює.

Готові рішення є й у вигляді пакетів, але для більшості Laravel-застосунків ThrottlesExceptions у черзі + короткі тайм-аути + кеш як запасний варіант покривають потреби.

Докладніше в документації: Martin Fowler: Circuit Breaker

Схожі питання