Symfony Messenger : Patterns Avances pour la Production
Symfony Messenger est simple a mettre en place, mais le déployer en production avec fiabilite demande de maîtriser certains patterns avances. Voici les stratégies eprouvees pour des systèmes 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)
Après 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 après 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 vérifié 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 système fragile en infrastructure fiable et observable.
