<? phpukraine СПІВБЕСІДИ
Пошук по платформі
SYMFONY · SENIOR

Як побудувати надійну retry-стратегію в Messenger?

Транспорт налаштовується з retry_strategy: кількість спроб, початкова затримка й множник; вичерпані повідомлення йдуть у failed transport, а логічні помилки відкидаються одразу через UnrecoverableMessageHandlingException.

Що відбувається з повідомленням, яке впало в handler?
Як відрізнити тимчасову помилку від логічної в черзі?
Як не втратити повідомлення після вичерпання спроб?
Messenger retry failed transport

Повтори в Messenger — частина конфігурації транспорту, а не коду обробника. retry_strategy задає кількість спроб, початкову затримку, множник і максимальну затримку, тобто експоненційний backoff. При помилці Messenger додає до повідомлення RedeliveryStamp з номером спроби й публікує його назад у транспорт із затримкою, тому лічильник живе в самому повідомленні й працює однаково з AMQP, Doctrine чи Redis.

Надійність починається з класифікації помилок. Тимчасові — таймаут, обрив зʼєднання, deadlock — повторювати є сенс. Логічні — сутність не існує, дані невалідні, картка відхилена — повторювати шкідливо: пʼять спроб із backoff лише відкладають той самий результат. Для них є UnrecoverableMessageHandlingException, яка одразу відправляє повідомлення у failed transport. Зворотний випадок — RecoverableMessageHandlingException, яка змушує повторити навіть після ліміту.

Failed transport — не смітник, а черга на ручний розбір. Після виправлення причини повідомлення повертають через messenger:failed:retry, безнадійні видаляють. На розмір цієї черги має бути алерт, бо саме він, а не кількість retry, показує реальні втрати.

Оскільки між спробами частина роботи могла виконатись, обробник мусить бути ідемпотентним: перевіряти стан агрегату перед дією і зберігати результат під унікальним ключем. Різні типи повідомлень варто розводити по різних транспортах із власними стратегіями: платежам потрібно більше спроб і довший backoff, ніж сповіщенням.

// config/packages/messenger.yaml
// framework:
//   messenger:
//     failure_transport: failed
//     transports:
//       payments:
//         dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
//         retry_strategy: { max_retries: 5, delay: 2000, multiplier: 3, max_delay: 300000 }
//       failed: 'doctrine://default?queue_name=failed'
//     routing:
//       App\Message\CapturePayment: payments

#[AsMessageHandler]
final class CapturePaymentHandler
{
    public function __invoke(CapturePayment $message): void
    {
        $order = $this->orders->find($message->orderId)
            ?? throw new UnrecoverableMessageHandlingException('Order gone'); // не повторювати

        if ($order->isCaptured()) {
            return; // ідемпотентність: повтор після часткового виконання безпечний
        }

        try {
            $this->gateway->capture($order->paymentId(), $order->total());
        } catch (GatewayTimeout | ConnectionException $e) {
            throw new RecoverableMessageHandlingException('Gateway unavailable', previous: $e); // повторити з backoff
        } catch (CardDeclined $e) {
            $order->markDeclined($e->reason());
            throw new UnrecoverableMessageHandlingException('Declined', previous: $e);
        }

        $order->markCaptured();
    }
}
Що retry живе в конфігурації транспорту, а не в try/catch усередині handler: max_retries, delay, multiplier, max_delay дають експоненційний backoff.
Розділення помилок на два класи: тимчасові (мережа, таймаут, deadlock) повторюємо, логічні (невалідні дані, сутність не існує) відкидаємо одразу через UnrecoverableMessageHandlingException.
Що RecoverableMessageHandlingException навпаки змушує повторити, навіть якщо ліміт вичерпано.
Що failed transport це не смітник, а черга для ручного розбору: messenger:failed:show, retry, remove, і на її розмір має бути алерт.
Що handler мусить бути ідемпотентним, бо між спробами частина роботи могла виконатись, і що для власної логіки можна реалізувати RetryStrategyInterface.
Реалізовувати повтори циклом try/catch у handler: воркер блокується на sleep, і немає ні backoff, ні обмеження спроб, ні failed transport.
Повторювати все підряд: валідаційна помилка після п'яти спроб з backoff лише відкладає неминуче й засмічує логи.
Не налаштувати failed transport: після вичерпання спроб повідомлення просто зникає з ack.
Ставити однакову затримку без множника: під час падіння зовнішнього API всі повідомлення повертаються одночасно і добивають його.
Ігнорувати, що повідомлення після redelivery може отримати інший воркер: локальний стан у handler між спробами не зберігається.
ПОРАДА

Сильна відповідь містить моніторинг: алерт на розмір failed-черги, а не тільки на кількість retry. І приклад, як ви розділили Unrecoverable і Recoverable у реальному обробнику.

оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Стратегія живе в конфігурації транспорту, а моніторити треба розмір failed-черги.