Symfony. Integrando el componente Messenger con RabbitMQ
Como integrar el componente Messenger con el sistema de colas RabbitMQ
En el artículo anterior realicé una introducción al componente Messenger de Symfony en donde expliqué sus principales elementos y cómo podemos emplearlo para implementar un sistema de mensajes y colas dentro de nuestra aplicación.
En este nuevo artículo quiero profundizar un poco más en él para ver cómo podemos integrar el componente Messenger con RabbitMQ, uno de los sistemas más populares de colas que nos va a permitir correr en segundo plano las tareas más pesadas de nuestra aplicación como por ejemplo la generación de thumbnails, el envío de emails, etc.
Así que, ¡vamos a ello!
Configurando Docker
Puesto que integrar un sistema de colas en nuestra aplicación mediante RabbitMQ implica la interacción de varias librerías dentro del sistema, lo más recomendable es dockerizar el proyecto de modo que no tengamos que preocuparnos de las dependencias ni de la famosa frase… “pues en mi ordenador funciona 😅😅😅”.
Así que vamos a emplear la siguiente configuración de Docker de cara a tener los siguientes containers y poder correr nuestro proyecto en Symfony:
nginx
, pues queremos que nuestro proyecto basado en Symfony corra sobrephp-fpm
necesitaremos un servidor NGINX.php
, donde configuraremosphp-fpm
y las variables de entorno para el proyecto al que accederemos a través del navegador.php-consume
, otro container de PHP que se encargará de consumir automáticamente los mensajes encolados por el proyecto mediante la ejecución del comandomessenger:consume
del componente Messenger.rabbitmq
, container donde ejecutar RabbitMQ.
Es decir, tendremos la siguiente arqutiectura:

De este modo, el archivo docker-compose.yml
presentará el siguiente aspecto:

Dockerfile-php
Este container será al que accedamos a través de NGINX por medio de nuestro navegador. Por tanto, necesitaremos tener instalado php-fpm
y puesto que más adelante querremos enviar mensajes a RabbitMQ, añadiremos la extensión amqp
de PHP mediante las instrucciones:
pecl install amqp
docker-php-ext-enable amqp
El contenido del archivo es el siguiente:

Dockerfile-php-consume
Este archivo contendrá la configuración del container donde se ejecutará el consumidor de la cola Rabbit donde se añadirán los mensajes. Puesto que el consumidor también estará en nuestro proyecto de Symfony, la configuración para este container es prácticamente la misma que la del anterior salvo por dos detalles:
- Añadiremos un script de consola llamado
message-consumer.sh
que se encargará de ejecutar el comando de consola que consumirá la cola RabbitMQ. Este comando estará declarado dentro de nuestra aplicación de Symfony como veremos más adelante.
#!/usr/bin/env bashsleep 10;/var/www/rocket/bin/console messenger:consume -vv >&1;
- Declararemos un
ENTRYPOINT
para este container de modo que se ejecute automáticamente el anterior comando de consola.

Dockerfile-nginx
En este archivo especificaremos la configuración de NGINX para correrlo dentro de su container.

El archivo default.conf
donde definiremos nuestro virtual host es el siguiente:

Dockerfile-rabbitmq
Finalmente, nuestro container para Rabbit estará definido en el archivo Dockerfile-rabbitmq
que tendrá lo siguiente:

Estructura de carpetas
Con todo lo anterior, la estructura de carpetas que os recomiendo para el proyecto es la siguiente:

Ejecutar el proyecto
Si todo ha ido correctamente podréis ejecutar el siguiente comando desde vuestra consola de cara a realizar el build de los containers de Docker y lanzar el proyecto:
docker-compose up -d --build
Una vez que Docker termine de construir cada imagen deberíais poder acceder al proyecto por medio de la URL:
http://localhost:8001
(el puerto 8001 es el que mapeamos desde el archivo docker-composer.yml
para NGINX).
Con esto ya estaríamos listos para instalar el componente Messenger de Symfony en nuestro proyecto y comenzar a enviar y consumir mensajes a RabbitMQ. ¡Vamos a verlo!
Componente Messenger y RabbitMQ
Una vez que ya tenemos nuestro proyecto corriendo con Docker lo siguiente que haremos será ir a la raíz de Symfony e instalar el componente Messenger mediante composer:
composer require messenger
Esto ejecutará de paso la recipe de Flex asociada generando el archivo config/packages/messenger.yaml
con el siguiente contenido:

Ahora empieza lo interesante.
Vamos a suponer que estamos creando un sistema de notificaciones de modo que se pueda enviar mensajes a distintos usuarios (este es el mismo ejemplo que usé en el anterior artículo hablando sobre Messenger).
Lo primero que necesitaremos será crear dentro de la carpeta Message
una clase que represente dichas notificaciones. En nuestro caso puede tener un código similar al siguiente:

A continuación crearemos el handler encargado de procesar ese mensaje dentro de la carpeta MessageHanlder
:

Gracias a que estamos implementando la interfaz MessageHandlerInterface
no es necesario etiquetar este servicio para que sea reconocido como un handler
por Messenger.
Y finalmente crearemos un Controller
que cree unos cuantos mensajes:


Si ahora accedemos a http://localhost:8001/notification veremos como por pantalla obtenemos lo siguiente:
Notification sent to one\nNotification sent to two\nNotification sent to three\nNotifications sent
Esto se debe a que todavía no hemos configurado el transport
para nuestros mensajes por lo que por defecto, los mensajes que enviamos son procesados de forma síncrona. Es aquí donde entra en acción RabbitMQ.
Integrando RabbitMQ
Dado que queremos que nuestros mensajes se procesen de forma asíncrona lo que haremos será añadir lo siguiente al archivo config/packages/messenger.yaml
:

- En la línea 7 estamos configurando el
transport
amqp
con el valor de la variable de entornoMESSENGER_TRANSPORT_DSN
que generamos en nuestra configuración de Docker y que contiene la URL desde la que acceder a Rabbit:
amqp://guest:guest@rabbitmq:5672/%2f/messages
- En la línea 10 especificamos que todos los mensajes de nuestra clase
NotificationMesssage
vayan a través deltransport
amqp
que hemos definido.
Por tanto, si ahora accedemos a la URL http://localhost:8001/notification veremos como la respuesta es inmediata y ya no obtenemos como respuesta los mensajes echo que genera el handler NotificationHandler
pues habrán sido enviados a RabbitMQ.
Revisando Rabbit
Vale, bien, los mensajes han ido a parar a RabbitMQ. ¿Cómo podemos comprobar que ha sido así?
Mediante Docker accederemos al container donde se está ejecutando RabbitMQ:
docker exec -it project_name_rabbitmq_1 bash
Y ahora desde el bash de ese container ejecutaremos el siguiente comando:
$ rabbitmqctl list_queues
Lo cual arrojará lo que sospechábamos: que hay mensajes encolados:
root@fcf3d72d0e56:/# rabbitmqctl list_queuesTimeout: 60.0 seconds ...Listing queues for vhost / ...name messagesmessages 1
En el caso de que queramos emplear la interfaz que proporciona RabbitMQ podremos acceder a http://localhost:15672/ y con las credenciales guest:guest
podremos ver cómo hay mensajes encolados así como el resto de estadísticas:

Eso sí, de momento tendremos la siguiente limitación: no podremos ver los consummers desde este panel debido a lo siguiente:
The consumers do not show up in an admin panel as this transport does not rely on
\AmqpQueue::consume()
which is blocking. Having a blocking receiver makes the--time-limit/--memory-limit
options of themessenger:consume
command as well as themessenger:stop-workers
command inefficient, as they all rely on the fact that the receiver returns immediately no matter if it finds a message or not. The consume worker is responsible for iterating until it receives a message to handle and/or until one of the stop conditions is reached. Thus, the worker's stop logic cannot be reached if it is stuck in a blocking call.
Consumiendo los mensajes
Finalmente si queremos consumir los mensajes encolados accederemos al container php
mediante el comando:
docker exec -it project_name_php_1 bash
Y navegaremos hasta la raíz del proyecto en /var/www/project_name
desde donde ejecutaremos el siguiente comando que nos proporciona el componente Messenger:
bin/console messenger:consume
Lo cual procesará los mensajes encolados (si los hay) mostrando por pantalla la instrucción echo
:
Notification sent to one\nNotification sent to two\nNotification sent to three
En el caso de que hayamos levantado también el container php-consumer
este proceso estará ejecutándose de forma automática por lo que si queremos ver cómo los mensajes se procesan a medida que llegan podemos ejecutar desde consola el siguiente comando:
docker logs -f project-name_php-consume_1 --details
Lo que nos mostrará por consola:
[OK] Consuming messages from transports "amqp".// The worker will automatically exit once it has received a stop signal via// the messenger:stop-workers command.
y conforme lleguen los mensajes obtendremos lo siguiente:
2019-09-16T10:51:28+00:00 [info] Received message App\Message\NotificationMessage2019-09-16T10:51:31+00:00 [info] Message App\Message\NotificationMessage handled by App\MessageHandler\NotificationHandler::__invoke2019-09-16T10:51:31+00:00 [info] App\Message\NotificationMessage was handled successfully (acknowledging to transport).
Sin embargo, puede que las instrucciones echo
dentro de nuestro NotificationHandler
no aparezcan (sospecho que es un tema de la configuración de salida) por lo que podéis modificar dicha clase del siguiente modo para obtener algo más de información:

Es decir, he sustituido los echo
por una llamada al Logger
de modo que ahora sí aparecerá por pantalla el mensaje Notification sent to
.
Conclusiones
Como veis el proceso para tener una aplicación de Symfony integrada con RabbitMQ y dockerizada para asegurarnos que todo funciona como esperamos es bastante sencillo.
A partir de aquí ya está en vuestra mano buscarle la aplicación en vuestros proyectos, especialmente dentro de aquellos procesos que sean más pesados de realizar como comentaba al principio del artículo.
¡Espero que os haya gustado y hasta el siguiente artículo!
¿Quieres recibir más artículos como este?
Si te ha gustado este artículo te animo a que te suscribas a la newsletter que envío cada domingo con publicaciones similares a esta y más contenido recomendado: 👇👇👇