<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Rabbitmq on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/rabbitmq/</link><description>Recent content in Rabbitmq on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Wed, 26 Jan 2022 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/tags/rabbitmq/index.xml" rel="self" type="application/rss+xml"/><item><title>Swarrot vs Symfony Messenger : une comparaison en conditions réelles</title><link>https://guillaumedelre.github.io/fr/2022/01/26/swarrot-vs-symfony-messenger-une-comparaison-en-conditions-r%C3%A9elles/</link><pubDate>Wed, 26 Jan 2022 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2022/01/26/swarrot-vs-symfony-messenger-une-comparaison-en-conditions-r%C3%A9elles/</guid><description>Swarrot et Symfony Messenger gèrent tous deux RabbitMQ en PHP. Voici pourquoi on a gardé Swarrot après avoir sérieusement évalué une migration.</description><content:encoded><![CDATA[<p>On a migré une plateforme de microservices médias vers Symfony 6 début 2022. Douze services, la plupart consommant des messages depuis RabbitMQ via <a href="https://github.com/swarrot/swarrot" target="_blank" rel="noopener noreferrer">Swarrot</a>. Symfony 6 a rendu <a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Messenger</a> plus central que jamais, et pendant la planification de la migration un développeur a posé la question évidente : pourquoi ne pas migrer en même temps ?</p>
<p>Ça vient avec le framework. Ça a de la logique de retry, du support AMQP natif, de la documentation first-party. Notre setup ressemblait à de l&rsquo;artisanat par comparaison.</p>
<p>Question légitime. On l&rsquo;a prise au sérieux. Voilà ce qu&rsquo;on a trouvé.</p>
<h2 id="câbler-la-topologie-à-la-main">Câbler la topologie à la main</h2>
<p>Swarrot est une bibliothèque consumer qui enveloppe l&rsquo;extension PECL AMQP. Elle lit des octets depuis une queue, les fait passer à travers une chaîne de processors (leur terme pour middleware), et laisse votre code décider quoi faire avec le payload. C&rsquo;est vraiment tout.</p>
<p>La chaîne de middleware est la partie intéressante. Les processors sont des décorateurs imbriqués, chacun enveloppant le suivant. Les couches extérieures gèrent les préoccupations d&rsquo;infrastructure avant même que le message n&rsquo;atteigne la logique métier :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">middleware_stack</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.signal_handler&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.max_execution_time&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.exception_catcher&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.doctrine_object_manager&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;swarrot.processor.ack&#39;</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">configurator</span>: <span style="color:#e6db74">&#39;app.processor.retry&#39;</span>
</span></span></code></pre></div><p><code>signal_handler</code> est en tête parce qu&rsquo;il doit intercepter <code>SIGTERM</code> avant que tout autre processor ne le voie. <code>ack</code> est près du bas parce qu&rsquo;on n&rsquo;acquitte le message qu&rsquo;après que le traitement réussit. L&rsquo;ordre n&rsquo;est pas arbitraire, et il est entièrement visible dans la configuration.</p>
<p>La topologie est tout aussi explicite. On déclare tout soi-même : exchanges, routing keys, queues de retry, queues de lettres mortes :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">messages_types</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_retry</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.retry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">content.ingest_dead</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exchange</span>: <span style="color:#ae81ff">e.app.content</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing_key</span>: <span style="color:#ae81ff">q.app.content.ingest.dead</span>
</span></span></code></pre></div><p>Trois entrées par type de message logique : queue principale, queue de retry, queue de lettres mortes. Tout ce qui existe sur le broker est nommé ici. La config est verbeuse mais honnête : pas d&rsquo;inférence, pas de convention plutôt que configuration. Si une queue existe dans RabbitMQ, on peut la tracer jusqu&rsquo;à une seule ligne de YAML.</p>
<h2 id="quand-le-nom-de-classe-devient-la-route">Quand le nom de classe devient la route</h2>
<p><a href="https://symfony.com/doc/current/messenger.html" target="_blank" rel="noopener noreferrer">Symfony Messenger</a> opère un niveau plus haut. On définit une classe de message, un handler, et un transport. La bibliothèque gère la sérialisation, le routing, le retry et les queues d&rsquo;échec automatiquement.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">IngestContent</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">__construct</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $contentId,
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">readonly</span> <span style="color:#a6e22e">string</span> $source,
</span></span><span style="display:flex;"><span>    ) {}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">framework</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">messenger</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">transports</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">async</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">dsn</span>: <span style="color:#e6db74">&#39;%env(MESSENGER_TRANSPORT_DSN)%&#39;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">retry_strategy</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">max_retries</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">delay</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">routing</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#39;App\Message\IngestContent&#39;</span>: <span style="color:#ae81ff">async</span>
</span></span></code></pre></div><p>Messenger sérialise l&rsquo;objet, le met sur le transport, et le désérialise de l&rsquo;autre côté dans la classe correcte. Pas de topologie manuelle, pas de noms d&rsquo;exchange explicites. Le nom de classe est la primitive de routing.</p>
<p>Cette dernière phrase est exactement là où les choses se sont compliquées pour nous.</p>
<h2 id="quand-le-typage-devient-du-couplage">Quand le typage devient du couplage</h2>
<p>Messenger suppose que le producteur et le consumer partagent une définition de classe PHP. C&rsquo;est bien pour une seule application, ou pour des services qui partagent un package de contrats dédié. Dans un monorepo d&rsquo;applications Symfony indépendantes, ça crée un couplage qui n&rsquo;existe tout simplement pas aujourd&rsquo;hui.</p>
<p>Prenez un message d&rsquo;ingestion de contenu que douze services consomment. Avec Swarrot, chaque service lit le payload JSON brut et prend les champs qui l&rsquo;intéressent. Ajouter un nouveau champ signifie mettre à jour le producteur. Les consumers qui n&rsquo;ont pas besoin du champ continuent de fonctionner sans modification.</p>
<p>Avec Messenger, <code>IngestContent</code> doit être définie quelque part que les douze services peuvent référencer. Ça signifie soit :</p>
<ul>
<li>Un package PHP partagé, versionné, déployé et maintenu à travers les services. Chaque changement de schéma devient un exercice de coordination inter-services.</li>
<li>Des classes dupliquées dans chaque service, qui divergent silencieusement sous la pression.</li>
</ul>
<p>Ni l&rsquo;une ni l&rsquo;autre n&rsquo;est gratuite. L&rsquo;approche package partagé inverse le modèle de propriété : le schéma de message devient une dépendance plutôt qu&rsquo;un contrat défini à la frontière. L&rsquo;approche duplication est juste le problème original différé.</p>
<p>La différence fondamentale est ce que représente un message. Messenger est conçu pour des <strong>commandes typées</strong> : un objet qui porte du sens et se distribue à un handler spécifique. Swarrot traite les messages comme des <strong>données opaques</strong> : des octets qui coulent à travers une topologie, traités par le consumer qui écoute. Si vos messages sont des données, l&rsquo;abstraction supplémentaire qu&rsquo;ajoute Messenger ne vous aide pas. Elle crée de la friction.</p>
<h2 id="le-bloquant">Le bloquant</h2>
<p>Le problème de sérialisation était le décisif. Dans un monorepo où les services sont autonomes, partager des classes PHP entre eux n&rsquo;est pas architecturalement neutre : c&rsquo;est une décision de couplage qui rend les changements futurs plus difficiles. On aurait échangé une bibliothèque nominalement &ldquo;legacy&rdquo; pour une plus moderne tout en introduisant exactement le genre de couplage fort qu&rsquo;on avait passé des années à éviter.</p>
<p>Il y avait des préoccupations secondaires aussi. L&rsquo;extension PECL AMQP donne un accès direct aux fonctionnalités du broker (priorités de messages, TTL par queue, routing par headers exchange) que Messenger abstrait. Et migrer quinze consumers sans jour J signifie faire tourner les deux bibliothèques en parallèle, ce qui est une vraie contrainte opérationnelle.</p>
<p>Mais le problème de sérialisation seul aurait suffi.</p>
<h2 id="données-ou-commandes--voilà-la-question">Données ou commandes : voilà la question</h2>
<p>Le choix ne concerne pas la qualité des bibliothèques. Messenger est bien maintenu, bien documenté, et s&rsquo;intègre proprement dans l&rsquo;écosystème Symfony.</p>
<p>La question à se poser en premier est : que sont vos messages ?</p>
<p>Si ce sont des commandes typées avec un schéma connu et un seul consumer faisant autorité, Messenger est un choix naturel. On écrit une classe, un handler, on configure un transport, et l&rsquo;infrastructure gère le reste.</p>
<p>Si ce sont des payloads de données consommés par plusieurs services indépendants, chacun possédant sa propre désérialisation, l&rsquo;abstraction qu&rsquo;ajoute Messenger joue contre vous. La topologie explicite de Swarrot et son modèle de payload brut donnent plus de contrôle là où on en a vraiment besoin.</p>
<p>Une vraie limitation à garder à l&rsquo;esprit : Swarrot est lié à l&rsquo;extension PECL AMQP, qui n&rsquo;implémente qu&rsquo;AMQP 0-9-1. Ce qui signifie que RabbitMQ (ou un broker compatible) est une dépendance dure. Si l&rsquo;infrastructure migre un jour vers un broker AMQP 1.0 (Azure Service Bus, ActiveMQ Artemis), Swarrot ne peut pas suivre. La couche transport de Messenger abstrait ça proprement : changer de broker signifie changer un DSN, pas réécrire les consumers.</p>
<p>Si la portabilité de broker est une exigence, ou susceptible de le devenir, ça change significativement le calcul.</p>
<p>Swarrot n&rsquo;est pas du legacy à migrer. Pour l&rsquo;instant, c&rsquo;est le bon choix : le routing AMQP comme primitive, les messages comme données, RabbitMQ comme choix d&rsquo;infrastructure long terme.</p>
<p>Ça pourrait changer. Un package de contrats partagé, une nouvelle exigence de broker, un service greenfield qui ne porte pas le poids de la topologie existante : n&rsquo;importe lequel de ces éléments pourrait faire pencher la balance vers Messenger. La bibliothèque n&rsquo;est pas inadaptée à cette plateforme. Elle est peut-être juste la bonne réponse pour une version future de celle-ci.</p>
]]></content:encoded></item></channel></rss>