De RabbitMQ à MySQL avec Symfony Messenger

Dans cet article technique, nous allons voir comment passer du gestionnaire de file RabbitMQ avec les bundles et librairies PHP associées vers le composant Symfony Messenger en utilisant des files stockées dans la base de données MySQL.

Pourquoi abandonner RabbitMQ au profit de MySQL ?

Tout d’abord parce que nous pouvons nous séparer de RabbitMQ, ce qui évitera de maintenir à jour une dépendance.
Ensuite et surtout, parce que la librairie php-amqp que nous utilisions n’était pas disponible pour PHP8 (et ne l’est toujours pas à l’heure actuelle) et qu’il y avait assez peu de visibilité sur la publication d’une version stable compatible, ce qui a retardé l’adoption de cette nouvelle mouture sur le projet.

Ce changement d’outil va nous permettre de simplifier la mise à jour et la maintenance de notre application.

Passer de RabbitMQ à MySQL : Comment faire ?

Dans la théorie et les grandes lignes, c’est plutôt simple. On installe le composant Symfony Messenger, on le configure, on transforme les consumer en handler et ça fonctionne… Sauf qu’on a un certain nombre de consumer. Une bonne dizaine. Commençons dans l’ordre.

La configuration du composant Messenger

La configuration du composant Messenger est plutôt simple. On la place dans config/packages/messenger.yaml.

framework:
    messenger:
        failure_transport: failed

        transports:
            async:
                dsn: 'doctrine://default'
                options:
                    auto_setup: false
                retry_strategy:
                    max_retries: 0
            failed: 'doctrine://default?queue_name=failed'

        routing:
            App\Domain\Command\AbstractAsyncCommand: async
        default_bus: command.bus
        buses:
            command.bus: ~

On défini deux transports pour le Messenger : async et failed. Il en existe en réalité un troisième, qui existe quoi qu’il arrive, qui consomme les messages de façon synchrone, comme si vous appeliez une méthode d’un de vos objets ou service.

Le transport nommé async va enregistrer les messages dans doctrine, c’est le dsn qui va définir ceci, on pourrait utiliser amqp:// pour continuer d’utiliser Rabbit par exemple. Symfony en propose quelques-uns et il est également possible de créer ses propres transporteurs. On indique que, si un message échoue (une exception est levée pendant le traitement), le Messenger ne tentera pas de réexécuter le message, il le placera alors dans la queue failed, qui est également enregistrée dans doctrine. C’est la configuration failure_transport qui indique quel transporteur utiliser lorsqu’un message n’a pas pu être traité.

Une autre configuration

Une autre configuration intéressante est : routing. Celle-ci va nous permettre de simplement faire en sorte qu’un message soit routé vers un transporteur ou un autre. En l’occurrence, si le message étend la classe AbstractAsyncCommand, plutôt que le message soit traité de façon synchrone, celui-ci sera routé vers le transporteur async qui enregistrera donc les données sérialisées du message dans la base de données.

Les modifications à apporter au code

Le fonctionnement entre le Messenger et RabbitMQ est un peu différent. Il ne s’agit plus de Consumer mais de Handler. Il ne s’agit plus de Publisher mais de la méthode dispatch sur un bus de commande.

<?php
// La commande
class SubscribeToNewsletter extends AbstractAsyncCommand
{
    // ...
}

// Dispatcher la commande depuis un contrôleur ou un service
class SubscribeToNewsletterController extends Controller
{
    public function __invoke(MessageBusInterface $commandBus)
    {
        $commandBus->dispatch(new SubscribeToNewsletter($this->getUser()->getId()));
    }
}

// Consommer le message grâce aux handler
class SubscribeToNewsletterHandler implements MessengerHandlerInterface
{
    public function __construct(/* des éventuelles dépendances */) {}
    public function __invoke(SubscribeToNewsletter $command)
    {
        // Faire des appel à une api par exemple
    }
}

La seule difficulté rencontrée serait la quantité de consumer qu’il faudrait potentiellement remplacer, le fonctionnement de chacun d’eux peut rester identique. Il faudra donc créer un nombre de commandes équivalent et remplacer les appels aux différents publisher Rabbit par l’appel à la méthode dispatch du nouveau bus.

L’impact sur la performance

Cela entraîne quelques différences sur la performance. En effet, le messenger, avec MySQL, va faire une requête régulièrement pour voir si des messages sont à traiter et les traiter. La requête est évidemment plus longue que le traitement via RabbitMQ qui a été conçu pour ça.

Il est possible que ceci soit amélioré en utilisant plutôt PostgreSQL car il dispose d’un système de notifications. C’est donc la base de donnée qui va informer le messenger d’un nouveau message et ainsi soulager un peu le serveur.

Bonne migration !

Retrouvez tous nos article tech ici !

Voir l’étude de cas
Lire l’article
Voir le témoignage
Fermer