src/EventsSubscriber/Worker/WorkerFailedMessageSubscriber.php line 33

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\EventsSubscriber\Worker;
  4. use Psr\Log\LoggerInterface;
  5. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  6. use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
  7. use Symfony\Component\Messenger\Exception\HandlerFailedException;
  8. /**
  9.  * Fail loud. Logs every message failure with its REAL exception class, and in particular every
  10.  * message that exhausts its retries and is routed to the `failed` dead-letter queue.
  11.  *
  12.  * This is the single, structured, greppable signal that work was dropped — the counterpart to the
  13.  * CloudWatch DLQ-depth alarm. It replaces the previous pattern of swallowed exceptions and
  14.  * unwatched logs: a dropped payout, email, or video check is now an ERROR line carrying the message
  15.  * class, transport, and underlying error, not silence.
  16.  *
  17.  * It logs to the dedicated `dlq` channel, which is shipped to stderr -> CloudWatch (the log
  18.  * counterpart to the DLQ-depth alarm) but excluded from the Sentry handler: exhausted-retry churn is
  19.  * expected, high-volume and 0-user-impact, so it must not consume Sentry quota. The underlying
  20.  * handler exceptions still reach Sentry on their own via the messenger integration — this is only the
  21.  * summary signal line, so dropping it from Sentry loses no diagnostic detail.
  22.  */
  23. class WorkerFailedMessageSubscriber implements EventSubscriberInterface
  24. {
  25.     public function __construct(private readonly LoggerInterface $dlqLogger)
  26.     {
  27.     }
  28.     public function onMessageFailed(WorkerMessageFailedEvent $event): void
  29.     {
  30.         $throwable $event->getThrowable();
  31.         // Messenger wraps handler exceptions in HandlerFailedException — unwrap to the real cause so
  32.         // the log (and Sentry grouping) reflect the actual error, not the generic wrapper.
  33.         $cause $throwable instanceof HandlerFailedException
  34.             ? ($throwable->getPrevious() ?? $throwable)
  35.             : $throwable;
  36.         $context = [
  37.             'message' => $event->getEnvelope()->getMessage()::class,
  38.             'transport' => $event->getReceiverName(),
  39.             'exception' => $cause::class,
  40.             'error' => $cause->getMessage(),
  41.             'willRetry' => $event->willRetry(),
  42.         ];
  43.         if ($event->willRetry()) {
  44.             $this->dlqLogger->warning('Messenger message failed; will retry'$context);
  45.             return;
  46.         }
  47.         $this->dlqLogger->error('Messenger message exhausted retries; routed to dead-letter (DLQ)'$context);
  48.     }
  49.     public static function getSubscribedEvents(): array
  50.     {
  51.         return [
  52.             WorkerMessageFailedEvent::class => 'onMessageFailed',
  53.         ];
  54.     }
  55. }