<?php
declare(strict_types=1);
namespace App\EventsSubscriber\Worker;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
/**
* Fail loud. Logs every message failure with its REAL exception class, and in particular every
* message that exhausts its retries and is routed to the `failed` dead-letter queue.
*
* This is the single, structured, greppable signal that work was dropped — the counterpart to the
* CloudWatch DLQ-depth alarm. It replaces the previous pattern of swallowed exceptions and
* unwatched logs: a dropped payout, email, or video check is now an ERROR line carrying the message
* class, transport, and underlying error, not silence.
*
* It logs to the dedicated `dlq` channel, which is shipped to stderr -> CloudWatch (the log
* counterpart to the DLQ-depth alarm) but excluded from the Sentry handler: exhausted-retry churn is
* expected, high-volume and 0-user-impact, so it must not consume Sentry quota. The underlying
* handler exceptions still reach Sentry on their own via the messenger integration — this is only the
* summary signal line, so dropping it from Sentry loses no diagnostic detail.
*/
class WorkerFailedMessageSubscriber implements EventSubscriberInterface
{
public function __construct(private readonly LoggerInterface $dlqLogger)
{
}
public function onMessageFailed(WorkerMessageFailedEvent $event): void
{
$throwable = $event->getThrowable();
// Messenger wraps handler exceptions in HandlerFailedException — unwrap to the real cause so
// the log (and Sentry grouping) reflect the actual error, not the generic wrapper.
$cause = $throwable instanceof HandlerFailedException
? ($throwable->getPrevious() ?? $throwable)
: $throwable;
$context = [
'message' => $event->getEnvelope()->getMessage()::class,
'transport' => $event->getReceiverName(),
'exception' => $cause::class,
'error' => $cause->getMessage(),
'willRetry' => $event->willRetry(),
];
if ($event->willRetry()) {
$this->dlqLogger->warning('Messenger message failed; will retry', $context);
return;
}
$this->dlqLogger->error('Messenger message exhausted retries; routed to dead-letter (DLQ)', $context);
}
public static function getSubscribedEvents(): array
{
return [
WorkerMessageFailedEvent::class => 'onMessageFailed',
];
}
}