Quorum queues não suportam global QoS e o Celery precisa do x-queue-type declarado
O que aconteceu
Nenhum worker de staging consumia nada. As filas acumulavam backlog com consumers = 0 — onion-staging com 118 mil mensagens, onion-staging-habits com 61 mil, onion-staging-progress com 42 mil.
O sintoma que expôs o problema foi o webhook de checkout: o endpoint respondia com task_id, a mensagem chegava em onion-staging-checkout, e nunca era processada. A investigação mostrou que não era um problema do checkout — os três workers estavam igualmente parados, em loop infinito de reconexão:
mingle: all alone
consumer: Connection to broker lost. Trying to re-establish the connection...
amqp.exceptions.AMQPNotImplementedError: Basic.consume: (540) NOT_IMPLEMENTED -
queue 'onion-staging-progress' in vhost 'onion-backend--staging' does not support global qos
Causa raiz
O broker de staging é Amazon MQ rodando RabbitMQ 4.2.9, e o vhost onion-backend--staging tem default_queue_type = quorum. Toda fila criada nesse vhost nasce quorum, mesmo quando o cliente não pede — e quorum queue não suporta global QoS.
O Celery 5.5 sabe contornar isso: Tasks.qos_global() desliga o global QoS quando detect_quorum_queues() encontra uma fila quorum. Só que a detecção lê apenas o queue_arguments das filas declaradas em task_queues:
python
# celery/utils/quorum_queues.py
for qname in queues:
qarguments = queues[qname].queue_arguments or {}
if qarguments.get("x-queue-type") == "quorum":
return True, qname
Como task_queues era None e as filas nasciam da declaração implícita feita a partir do -Q "${CELERY_QUEUES}" (run/runtime), o Celery as tratava como clássicas, mantinha apply_global=True no basic.qos, o RabbitMQ rejeitava o basic.consume e a conexão caía — para sempre.
O ponto não óbvio: o tipo real da fila no broker não importa para a detecção. O Celery decide pelo que ele mesmo declarou, não pelo que existe no servidor. Uma fila que é quorum no RabbitMQ mas foi declarada sem x-queue-type é, para o Celery, clássica.
Correção
config/queues.py ganhou quorum_task_queues(), e config/settings/staging.py passou a declarar as filas explicitamente:
python
CELERY_TASK_QUEUES = quorum_task_queues(["default", "habits", "progress", "checkout", "analytics"])
CELERY_TASK_DEFAULT_QUEUE = resolve_queue("default")
Contexto da implementação:
- A declaração espelha bit a bit o que já existia no broker — exchange direct homônimo, routing key igual ao nome da fila — verificado nos bindings do vhost antes da mudança. Declaração equivalente não gera
PRECONDITION_FAILEDnem muda roteamento -Qcontinua filtrando o consumo: declarar as cinco filas emtask_queuesnão faz o worker consumir as cinco. Quem manda no consumo équeues.consume_from, populado peloselect()do-QCELERY_TASK_DEFAULT_QUEUEentrou junto porque o podwebsobe comCELERY_QUEUES=tasks, o que fazia o default do produtor ser a filatasks— que ninguém consome, e que explicava 17 mil mensagens órfãs no vhost
Como evitar
- A correção não pode ser compartilhada entre ambientes com versões diferentes de broker. O compose local usa
rabbitmq:3.13, cujo default é fila classic: declararx-queue-type: quorumnum settings compartilhado faria o Celery redeclarar filas locais existentes como quorum, resultando emPRECONDITION_FAILED. Por isso a mudança ficou confinada emconfig/settings/staging.py— produção usa SQS e oconfig/settings/__init__.pydespacha porENV, entãostaging.pynunca é importado lá - Ao subir ou migrar um broker RabbitMQ 4.x, checar o
default_queue_typedo vhost antes de assumir que as filas serão classic - Fila com backlog crescente e
consumers = 0é sinal de consumidor caindo no handshake, não de produtor errado. Otask_idretornado pela API só prova que a mensagem foi publicada — nunca que existe alguém consumindo - Ao declarar
task_queuesmanualmente, replicar exchange e routing key da declaração implícita do Celery, senão a mudança de roteamento silenciosamente órfã as mensagens já enfileiradas