Повтори в 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();
}
}
Сильна відповідь містить моніторинг: алерт на розмір failed-черги, а не тільки на кількість retry. І приклад, як ви розділили Unrecoverable і Recoverable у реальному обробнику.