← Voltar ao blog

Consumers do RabbitMQ no Magento 2 que não morrem em silêncio

Rodrigo Biazzi

Filas de mensagens no Magento são fáceis de configurar e fáceis de quebrar sem ninguém notar. Um consumer para, nada dá erro na loja, e dias depois alguém pergunta por que os pedidos não foram exportados ou por que as ações em massa no admin nunca terminaram. A fila ficou crescendo no fundo.

Este artigo mostra como a configuração de filas se encaixa no Magento 2.4, como os consumers são iniciados e quais ajustes e gerenciamento de processos os mantêm rodando. Tudo aqui vale para Magento Open Source e Adobe Commerce com RabbitMQ como broker.

Conectando o Magento ao RabbitMQ

A conexão AMQP fica em app/etc/env.php. Dá para gravar usando o comando de setup em vez de editar o arquivo na mão:

bin/magento setup:config:set \
  --amqp-host=rabbitmq \
  --amqp-port=5672 \
  --amqp-user=magento \
  --amqp-password='change-me' \
  --amqp-virtualhost=/

Sem esse bloco, o Magento usa a fila em MySQL onde for possível. Confira qual backend você está realmente usando antes de depurar qualquer outra coisa, porque as ferramentas e os sintomas são diferentes.

Os quatro arquivos XML

Uma fila no Magento é descrita por quatro arquivos na pasta etc do módulo. Cada um responde uma pergunta diferente.

communication.xml: qual é a mensagem

Declara o nome do topic e o tipo de dado que ele carrega. O tipo da requisição pode ser um escalar como string ou uma interface com getters e setters, que o Magento serializa para você.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd">
    <topic name="vendor.order.export" request="string"/>
</config>

queue_publisher.xml: para onde vai

Diz ao Magento para qual conexão e exchange um topic é publicado.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd">
    <publisher topic="vendor.order.export">
        <connection name="amqp" exchange="magento"/>
    </publisher>
</config>

queue_topology.xml: como é roteada

Liga o topic na exchange a uma fila. O Magento cria a exchange, a fila e o binding no RabbitMQ quando você roda setup:upgrade.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd">
    <exchange name="magento" type="topic" connection="amqp">
        <binding id="vendorOrderExportBinding"
                 topic="vendor.order.export"
                 destinationType="queue"
                 destination="vendor.order.export"/>
    </exchange>
</config>

queue_consumer.xml: quem lê

Dá nome ao consumer, indica a fila que ele lê e o método handler que processa cada mensagem.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd">
    <consumer name="vendor.order.export"
              queue="vendor.order.export"
              connection="amqp"
              maxMessages="1000"
              consumerInstance="Magento\Framework\MessageQueue\Consumer"
              handler="Vendor\OrderExport\Model\Queue\ExportHandler::process"/>
</config>

Rode bin/magento queue:consumers:list depois do setup:upgrade para confirmar que o consumer está registrado.

Publicando e tratando mensagens

Publicar é uma única chamada em Magento\Framework\MessageQueue\PublisherInterface. Publique ids, não objetos inteiros. A entidade pode mudar entre a publicação e o consumo, e mensagens pequenas são baratas de repetir.

<?php
declare(strict_types=1);

namespace Vendor\OrderExport\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\MessageQueue\PublisherInterface;

class QueueOrderExport implements ObserverInterface
{
    public function __construct(
        private readonly PublisherInterface $publisher
    ) {
    }

    public function execute(Observer $observer): void
    {
        $order = $observer->getEvent()->getOrder();
        $this->publisher->publish('vendor.order.export', (string)$order->getId());
    }
}

O handler recebe a mensagem já decodificada. Faça ele idempotente, porque uma mensagem pode ser entregue mais de uma vez, por exemplo quando o processo morre depois de fazer o trabalho mas antes de o acknowledgement chegar ao broker.

<?php
declare(strict_types=1);

namespace Vendor\OrderExport\Model\Queue;

use Psr\Log\LoggerInterface;
use Vendor\OrderExport\Model\Exporter;

class ExportHandler
{
    public function __construct(
        private readonly Exporter $exporter,
        private readonly LoggerInterface $logger
    ) {
    }

    public function process(string $orderId): void
    {
        if ($this->exporter->isExported((int)$orderId)) {
            return;
        }

        try {
            $this->exporter->export((int)$orderId);
        } catch (\Throwable $e) {
            $this->logger->error('Order export failed', [
                'order_id' => $orderId,
                'exception' => $e,
            ]);
            $this->exporter->markForRetry((int)$orderId);
        }
    }
}

O bloco catch importa. Com a classe de consumer padrão, quando o handler lança uma exceção, a mensagem é rejeitada e não volta para a fila. Se você não registrar a falha em algum lugar, ela simplesmente some. Um log mais uma flag de retry na sua própria tabela dá algo concreto para agir.

Como os consumers são iniciados

Consumers são processos PHP de longa duração iniciados com bin/magento queue:consumers:start. Há duas formas de mantê-los vivos: deixar o cron do Magento iniciá-los ou rodá-los sob um gerenciador de processos.

O runner de consumers do cron

Por padrão, o cron job consumers_runner roda a cada minuto e inicia todo consumer que ainda não esteja rodando. Ele é controlado pela seção cron_consumers_runner do env.php:

'cron_consumers_runner' => [
    'cron_run' => true,
    'max_messages' => 10000,
    'consumers' => [
        'vendor.order.export',
        'async.operations.all',
        'product_action_attribute.update',
    ],
],
'queue' => [
    'consumers_wait_for_messages' => 1,
],
  • cron_run liga ou desliga o runner.
  • max_messages é quantas mensagens um consumer trata antes de sair. O próximo cron inicia um novo. Isso limita o crescimento de memória e faz o processo carregar código novo depois de um deploy.
  • consumers limita quais consumers o cron inicia. Um array vazio significa todos. Listá-los explicitamente evita subir dezenas de processos que você não usa.
  • consumers_wait_for_messages com valor 1 mantém o consumer esperando novas mensagens. Com 0, o consumer sai assim que a fila esvazia, o que economiza memória em servidores pequenos ao custo de até um minuto de atraso.

O ponto fraco fica óbvio quando você enxerga: isso só funciona enquanto o cron do Magento roda. Se a linha do cron sumiu do crontab depois de uma migração de servidor, ou o processo do cron está preso em um lock, nenhum consumer é reiniciado depois que sai. Nada na loja avisa.

Supervisor ou systemd

Para consumers importantes para o negócio, um gerenciador de processos é mais previsível. Ele reinicia o processo quando sai por qualquer motivo, e você vê o estado com um comando. Desative o cron para esses consumers, com cron_run em false ou deixando eles fora da lista consumers, para nunca ter dois donos do mesmo processo.

[program:magento-order-export]
command=/usr/bin/php /var/www/shop/current/bin/magento queue:consumers:start vendor.order.export --single-thread --max-messages=1000
directory=/var/www/shop/current
user=www-data
numprocs=1
autostart=true
autorestart=true
startsecs=5
stopwaitsecs=60
redirect_stderr=true
stdout_logfile=/var/log/supervisor/magento-order-export.log

O mesmo com systemd, como uma unit de template para que um arquivo atenda vários consumers:

# /etc/systemd/system/[email protected]
[Unit]
Description=Magento consumer %i
After=network.target

[Service]
User=www-data
WorkingDirectory=/var/www/shop/current
ExecStart=/usr/bin/php bin/magento queue:consumers:start %i --single-thread --max-messages=1000
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
systemctl enable --now [email protected]
systemctl status [email protected]

Mantenha o --max-messages mesmo com um gerenciador de processos. Uma saída limpa e reinício a cada alguns milhares de mensagens é a proteção mais simples contra vazamento de memória, conexões de banco obsoletas e código antigo ainda carregado depois de um release.

Por que consumers morrem em silêncio

Estas são as causas que vale checar primeiro, mais ou menos na ordem em que aparecem na prática.

  • O cron não está rodando. O consumer saiu depois de max_messages e nada o iniciou de novo. Veja o crontab -l do usuário do Magento e a tabela cron_schedule em busca de entradas recentes de consumers_runner.
  • O handler lança exceção. A mensagem é rejeitada e perdida. Olhe em var/log/system.log e var/log/exception.log.
  • Erros fatais. Um limite de memória ou um type error mata o processo PHP. Sem --max-messages e sem um gerenciador de processos, ele continua morto.
  • Conexão com o banco perdida. Um consumer que espera muito tempo pode bater no wait_timeout do MySQL. Reiniciar depois de um número fixo de mensagens reduz essa janela.
  • Código antigo depois de um deploy. Um consumer em execução mantém as classes que carregou na partida. Reinicie todos os consumers em todo release.
  • Configuração de conexão quebrada. Credenciais ou virtual host errados no env.php, ou uma mudança de firewall. O consumer falha na partida toda vez que é lançado e só os logs mostram.

Monitore a fila, não o processo

Um processo rodando não prova que as mensagens estão fluindo. O sinal útil é a profundidade da fila e o número de consumers ligados a cada uma. No lado do RabbitMQ:

rabbitmqctl list_queues -p / name messages messages_unacknowledged consumers

Crie alerta quando uma fila importante tiver zero consumers, ou quando o número de mensagens continuar crescendo por mais tempo do que o seu processamento normal leva. O plugin de management do RabbitMQ expõe os mesmos números por HTTP se o seu monitoramento preferir assim.

No lado do Magento, ps aux | grep queue:consumers:start mostra o que está rodando, e a tabela queue_lock guarda os locks de mensagens se você precisar entender processamento duplicado.

Checklist de deploy

  • Rode setup:upgrade para criar novas exchanges, filas e bindings.
  • Reinicie todos os consumers depois da troca de código, com supervisorctl restart ou systemctl restart.
  • Se o cron é dono dos consumers, mate os processos antigos e deixe o próximo cron iniciá-los já no código novo.
  • Confira a profundidade da fila alguns minutos depois.

FAQ

Preciso do RabbitMQ para usar filas de mensagens no Magento 2?

Não. O Magento Open Source pode usar o MySQL como backend de fila para muitos topics. O RabbitMQ é recomendado em produção porque lida melhor com volume e alguns recursos esperam uma conexão AMQP.

O que o max-messages faz no queue:consumers:start?

Define quantas mensagens o consumer processa antes de sair. Combinado com o cron ou um gerenciador de processos que o reinicia, isso limita o crescimento de memória e garante que o código novo seja carregado depois dos deploys.

Devo usar cron_consumers_runner ou supervisor?

O cron serve para consumers de baixa prioridade. Para os consumers dos quais o negócio depende, supervisor ou systemd reiniciam na hora e deixam o estado visível. Não deixe os dois gerenciarem o mesmo consumer.

Por que as mensagens se acumulam no RabbitMQ sem erro no Magento?

Em geral nenhum consumer está ligado à fila. Veja a coluna consumers do rabbitmqctl list_queues, depois confira se o cron do Magento roda ou se o seu gerenciador de processos está de pé.

Sou Rodrigo Biazzi, desenvolvedor Magento certificado, e trabalho com Magento e Adobe Commerce há mais de 12 anos. Atendo como freelancer em Magento e Adobe Commerce, de correções pontuais a projetos mais longos. Se esse é o tipo de problema da sua loja, fale comigo.

Artigos relacionados