Symfony Messenger : Patterns Avances pour la Production
Symfony Messenger est simple à mettre en place, mais le deployer en production avec fiabilite demande de maitriser certains patterns avances. Voici les strategies eprouvees pour des systemes robustes.
1. Retry Strategy intelligente
La configuration par defaut retente 3 fois avec un delai exponentiel. En production, affinez selon le type d'erreur :
# config/packages/messenger.yaml
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 5
delay: 1000
multiplier: 3
max_delay: 60000
async_priority_high:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
queue_name: high
retry_strategy:
max_retries: 10
delay: 500
multiplier: 2
Retry conditionnel
Certaines erreurs ne meritent pas de retry (validation, 404...). Utilisez un RecoverableExceptionInterface :
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
#[AsMessageHandler]
class PaymentHandler
{
public function __invoke(ProcessPayment $message): void
{
try {
$this->gateway->charge($message->amount);
} catch (InvalidCardException $e) {
// Ne pas retenter : erreur permanente
throw new UnrecoverableMessageHandlingException($e->getMessage(), 0, $e);
}
// Les autres exceptions seront retentees automatiquement
}
}
2. Dead Letter Queue (DLQ)
Apres epuisement des retries, les messages atterrissent dans la failure transport. Mettez en place un monitoring :
framework:
messenger:
failure_transport: failed
transports:
failed:
dsn: 'doctrine://default?queue_name=failed'
# Consulter les messages echoues
php bin/console messenger:failed:show
# Retenter un message specifique
php bin/console messenger:failed:retry 42
# Supprimer un message
php bin/console messenger:failed:remove 42
Alerting automatique
#[AsMessageHandler]
class FailedMessageAlertHandler
{
public function __construct(private NotifierInterface $notifier) {}
public function __invoke(FailedMessageEvent $event): void
{
$notification = new Notification(
sprintf('Message echoue : %s', get_class($event->getEnvelope()->getMessage())),
['chat/slack']
);
$this->notifier->send($notification);
}
}
3. Middleware custom
Les middlewares interceptent chaque message avant et apres le handler :
class AuditMiddleware implements MiddlewareInterface
{
public function __construct(private LoggerInterface $logger) {}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$message = $envelope->getMessage();
$this->logger->info('Processing {class}', [
'class' => get_class($message),
'id' => $envelope->last(TransportMessageIdStamp::class)?->getId(),
]);
$startTime = microtime(true);
try {
$envelope = $stack->next()->handle($envelope, $stack);
} finally {
$duration = microtime(true) - $startTime;
$this->logger->info('Processed in {duration}ms', [
'duration' => round($duration * 1000, 2),
]);
}
return $envelope;
}
}
4. Scaling : workers et supervision
Supervisord
[program:messenger-worker]
command=php /var/www/app/bin/console messenger:consume async --time-limit=3600 --memory-limit=256M
autostart=true
autorestart=true
numprocs=3
process_name=%(program_name)s_%(process_num)02d
stdout_logfile=/var/log/messenger_%(process_num)02d.log
Signals et graceful shutdown
Les options --time-limit et --memory-limit permettent un recyclage propre des workers. Combinez avec pcntl_signal pour un arret graceful :
# Le worker finit le message en cours avant de s'arreter
php bin/console messenger:consume async --time-limit=3600
5. Patterns de serialisation
Messages versionnes
Pour evoluer vos messages sans casser les workers en cours :
// V1
class OrderPlaced {
public function __construct(public string $orderId) {}
}
// V2 - compatible backward
class OrderPlaced {
public function __construct(
public string $orderId,
public ?string $channel = null, // nouveau champ, nullable
) {}
}
Serialisation custom pour l'interop
framework:
messenger:
transports:
external_events:
dsn: '%env(RABBITMQ_DSN)%'
serializer: App\Messenger\ExternalEventSerializer
6. Monitoring en production
- Prometheus + Grafana : metriques custom via
EventSubscribersur les events Messenger - Symfony Profiler : panel Messenger pour le dev
- Healthcheck : endpoint qui verifie que les workers consomment
// Healthcheck simple
#[Route('/health/messenger')]
class MessengerHealthController
{
public function __invoke(Connection $connection): JsonResponse
{
$count = $connection->executeQuery(
"SELECT COUNT(*) FROM messenger_messages WHERE delivered_at IS NULL AND available_at <= NOW()"
)->fetchOne();
return new JsonResponse([
'pending_messages' => $count,
'healthy' => $count < 1000,
], $count < 1000 ? 200 : 503);
}
}
Conclusion
Messenger en production, c'est : retry intelligent, DLQ avec monitoring, workers supervises avec limites, et serialisation versionnee. Ces patterns transforment un systeme fragile en infrastructure fiable et observable.
