ADR-022: Filas em PostgreSQL — Revogação do RabbitMQ
Status: Aceita Data: 2026-07-04 Aceita em: 2026-07-07
O estudo técnico completo que fundamenta esta decisão — prós, contras, armadilhas, opções descartadas e impacto detalhado no repositório e na Stega — está em
references/ADR-022-estudo-filas-postgresql.md. Esta ADR decide o mecanismo de filas; a topologia de instâncias (quantos Postgres, um por finalidade) é decidida separadamente na ADR-023 — aceita no mesmo dia. A ADR-008 (RabbitMQ) estáSubstituída por ADR-022a partir desta aceitação.
Revisão 2026-07-07 — riscos analisados e aceitos: os pontos de decisão da
seção 11 do estudo anexo que pertencem a esta ADR foram revisados
explicitamente com o usuário e os riscos estão aceitos como propostos:
(1) o mecanismo de tick via processo próprio de longa duração (Deployment,
réplica única obrigatória — o PgQue não coordena tickers externos
concorrentes), aceitando o custo de mais um processo persistente em troca de
manter a imagem postgres:17-alpine intocada; (2) o risco de maturidade do
PgQue (v0.2.0, "early-stage"), aceito com a Opção C do estudo (Postgres puro,
sem PgQue) mantida como plano B documentado caso o risco se materialize.
Nenhuma questão pendente resta nesta ADR — status alterado para Aceita e
implementação concluída na Stega no mesmo dia.
Revisão 2026-07-08 — correção: a "paridade nativa completa" não se estende ao
Minion: a validação da implementação em Windows nativo (sem Docker) encontrou
Minion workers do not support fork emulation ao rodar carton exec perl script/stega minion worker e, por extensão, qualquer teste que chame
$app->minion->perform_jobs (t/030_webhooks.t, t/070_notifications.t).
Causa: Minion.pm::worker() (o módulo Minion em si, não código desta
aplicação) recusa operar — croak 'Minion workers do not support fork emulation' — em qualquer Perl com $Config{d_pseudofork} verdadeiro, o caso do
Strawberry/berrybrew no Windows (fork emulado via ithreads, sem fork() real do
SO). Isso não é causado por esta ADR nem introduzido pela migração para
PgQue — é uma restrição pré-existente do próprio Minion, presente desde antes
desta ADR e independente do mecanismo de fila escolhido (existiria da mesma
forma com RabbitMQ). O bullet "paridade nativa completa em qualquer
plataforma" na seção Positivo abaixo estava incorreto por generalizar demais:
é verdade que PgQue não exige compilador C nem exceção de plataforma, mas o
Minion (ADR-008, inalterado por esta ADR) continua exigindo Docker Compose ou
WSL2 para o processo minion worker especificamente no Windows nativo — ver a
correção equivalente na ADR-014, seção Negativo. Testes afetados usam
plan skip_all => ... if $Config{d_pseudofork} para não falhar falsamente
nesse ambiente. Resolver essa exceção de vez — não só contorná-la — é
pendência registrada na
ADR-024 (Proposta, sem
decisão ainda; inclui um inventário de uso real do Minion nesta aplicação e a
pergunta em aberto sobre se o PgQue sozinho cobriria os mesmos cenários).
Contexto
A ADR-008 define RabbitMQ, acessado via
Net::AMQP::RabbitMQ, como message broker do stack — usado para o papel de log de
eventos multi-consumidor (fan-out) que o Minion (fila de jobs interna, já em
PostgreSQL) não cobre.
Na prática, essa decisão tem um problema recorrente e documentado: Net::AMQP::RabbitMQ
é um módulo XS que embute a biblioteca C librabbitmq, e não compila em Windows
— falha de link (undefined reference to 'poll'), não de teste, sem flag que
contorne. Essa era uma exceção registrada no Guia 8
(rodar o worker via Docker Compose no Windows, enquanto o resto da aplicação roda
com Perl nativo) — removida com a aceitação desta ADR (ver "Ações necessárias").
Nenhuma alternativa Perl resolve isso sem trocar um problema por
outro: Mojo::RabbitMQ::Client está arquivado (2025-01-24, sem manutenção desde
2019); AnyEvent::RabbitMQ é pure-Perl no protocolo, mas sua interface síncrona
usual (Net::RabbitFoot) depende de Coro — outro módulo XS com histórico
problemático no Windows. O levantamento completo, incluindo issues do GitHub que
confirmam esse problema fora do nosso ambiente, está na seção 1 do estudo anexo.
Ao mesmo tempo, o stack já prova, em produção, que PostgreSQL sozinho resolve
fila de forma confiável: o Minion (ADR-008, nota "Minion como alternativa simples")
usa Minion::Backend::Pg, que implementa SELECT ... FOR UPDATE SKIP LOCKED +
LISTEN/NOTIFY — exatamente o padrão que a documentação oficial do PostgreSQL 17
recomenda para filas (sql-select.html: "can be used to avoid lock contention with
multiple consumers accessing a queue-like table" — tradução: "pode ser usado para
evitar disputa de lock com múltiplos consumidores acessando uma tabela do tipo
fila"). O que falta ao Minion é o papel
específico de fan-out multi-consumidor que hoje só o RabbitMQ cobre.
Decisão
Revogar a decisão de usar RabbitMQ (ADR-008) e adotar PostgreSQL como banco único
do stack para dados relacionais (ADR-007), documentos JSONB (ADR-017) e filas —
eliminando RabbitMQ, Net::AMQP::RabbitMQ e qualquer cliente AMQP do stack.
Mecanismo concreto proposto: PgQue (SQL puro + PL/pgSQL, licença Apache-2.0, pgque.dev) assume o papel de log de eventos multi-consumidor que o RabbitMQ tem hoje. Minion permanece inalterado para a fila de jobs interna — as duas camadas continuam distintas por escopo, só que ambas agora vivem inteiramente em PostgreSQL:
HTTP Handler → $c->minion->enqueue(...)
↓ (fila no Postgres, inalterado — Minion::Backend::Pg)
Minion Worker → pgque.send('stega.notifications', ...)
↓ (log de eventos no Postgres — PgQue)
NotificationWorker (pgque.receive/ack, cursor próprio)
Tick de rotação: um processo próprio, de longa duração (Deployment,
mesmo molde do NotificationWorker/worker do Minion — não um CronJob do
Kubernetes) chama pgque.ticker() em loop apertado (~100ms-1s), mais
pgque.maint()/maint_retry_events() periodicamente (~30s) e
pgque.maint_rotate_tables_step2() periodicamente (~10s), sempre em transação
própria — não pg_cron. O step2 não é opcional nem está embutido no
maint(): maint() o pula deliberadamente (o PgQ exige step1 e step2 em
transações separadas), e a rotação seguinte só acontece depois que o step2
registrou a anterior — um ticker externo que o omita faz uma rotação e
nunca mais rotaciona, e o anti-bloat (a razão central desta escolha) para
silenciosamente (ver seção 11 do estudo anexo, que verifica isso no
código-fonte; a página de instalação do próprio PgQue, "Option C — manual or
external scheduler", omite o step2 — a receita a replicar é o que
pgque.start() agenda no pg_cron, não essa página).
pgque.ticker_loop() é de uso interno do pg_cron (é o que ele agenda a cada
1s, e essa procedure faz sub-tick de 100ms por dentro); um processo externo
chama pgque.ticker() diretamente, com commit entre uma chamada e a próxima
(com Mojo::Pg, cada query() fora de transação já é autocommit). Um
CronJob do Kubernetes não atingiria a
cadência necessária sozinho (granularidade mínima de 1 minuto) — só serviria
se o próprio comando implementasse um laço interno de alta frequência antes de
sair, e nesse caso um Deployment sempre ativo é mais simples (sem lidar com
sobreposição de execuções nem com o laço precisar terminar antes do próximo
disparo). Essa escolha evita exigir uma imagem PostgreSQL customizada ou
alterar shared_preload_libraries; a imagem postgres:17-alpine (ADR-007/
ADR-014) permanece sem modificação para db-events, ao custo explícito de mais
um processo persistente a construir e operar.
Restrição de implementação obrigatória: exatamente uma réplica desse
processo (replicas: 1, mesma configuração já usada para o
notification-worker — ver Guia 9). O PgQue não protege pgque.ticker()
contra chamadas concorrentes quando o agendamento é externo (isso só é
garantido pelo próprio pg_cron quando o tick roda dentro dele) — duas
réplicas chamando ticker() ao mesmo tempo não têm nenhuma coordenação entre
si por parte do PgQue.
Ver seção 11 do estudo anexo para o raciocínio completo por trás desta escolha.
Instalação: um script de bootstrap dedicado (eng/bootstrap_pgque.pl —
deliberadamente fora de migrations/ e do fluxo de Mojo::Pg::Migrations, por
ser instalação de um pacote de terceiros, não uma migration de domínio) carrega
pgque.sql uma vez e concede o papel pgque_admin (já contém pgque_reader/
pgque_writer) à credencial que vai efetivamente se conectar ao banco onde o
PgQue vive — não necessariamente stega_app/stega_migrate: esta ADR decide o
mecanismo de filas, não em qual instância/credencial ele roda. A ADR-023 (também
Proposta) decide isso: se aceita, o PgQue vive numa instância db-events
dedicada com sua própria credencial única (D), sem nenhuma relação com as
credenciais de db-app. Em Kubernetes é um InitContainer separado do de migration
(bootstrap-pgque, não migrate); em Docker Compose, um serviço bootstrap-pgque
separado do serviço migrate — mesmo mecanismo de passo idempotente, script e nome
distintos, para não confundir "instalar o PgQue" com "aplicar migrations do
domínio" (ver ADR-023 para a topologia e as credenciais completas).
Justificativa
O estudo completo — API do PgQue, comparação com PgQ (Skytools) e com Postgres sem
nenhuma extensão, armadilhas de bloat/vacuum/WAL sob carga sustentada, e o
levantamento de que nenhum cliente Perl AMQP resolve o problema de Windows sem
reintroduzir outra dependência XS problemática — está em
references/ADR-022-estudo-filas-postgresql.md.
Referências: PostgreSQL, PgQue, PgQ, Minion, Mojo::Pg.
Alternativas Consideradas
| Alternativa | Motivo da rejeição |
|---|---|
| Manter RabbitMQ (status quo, ADR-008) | O problema de build no Windows não é do nosso ambiente — é documentado externamente (issue #144 do Net::AMQP::RabbitMQ) e não tem solução no cliente Perl sem reintroduzir outra dependência XS problemática (Coro, via Net::RabbitFoot). Mantém permanentemente uma exceção "Docker-only" para um componente, contrariando o objetivo de paridade nativa da ADR-014 |
PgQ original (Skytools, pgq/pgq) | Extensão C + daemon externo pgqd — reintroduz a mesma classe de fricção operacional (peça de infraestrutura C a compilar e manter) que estamos tentando eliminar, só do lado do servidor Postgres em vez do cliente Perl. Mantido sem desenvolvimento ativo desde 2023 |
PostgreSQL sem nenhuma extensão (SKIP LOCKED + LISTEN/NOTIFY + tabela própria) | Tecnicamente viável e zero dependência de terceiros, mas exige construir e manter, por conta própria, tudo que a PgQue já oferece pronto: rotação anti-bloat, dead-letter queue, fan-out multi-consumidor, observabilidade. Mesmo princípio já aplicado na ADR-016 (preferir mecanismo de biblioteca testada a utilitário próprio equivalente). Mantida como plano B caso a maturidade da PgQue (v0.2.0) seja considerada risco alto demais |
| Outro broker cross-platform (NATS, Redis Streams) | Resolveria o problema de cliente Perl no Windows, mas adiciona um novo serviço de backing ao stack — o oposto do objetivo desta proposta, que é consolidar em um único banco |
| Apenas Minion, sem substituto para o RabbitMQ | Minion não oferece fan-out multi-consumidor nem modelo de tópico/roteamento — continuaria sem solução para o caso de uso de notificação externa desacoplada que motivou o RabbitMQ na ADR-008 original |
Consequências
Positivo:
- Um único serviço de backing (PostgreSQL) para dados relacionais, documentos e filas — um serviço a menos para operar, monitorar e manter credenciais
- Elimina completamente a dependência XS/C (
librabbitmq) do stack — nenhum módulo de fila exige compilador C no ambiente de desenvolvimento - Remove a exceção "Windows nativo precisa de Docker" ligada especificamente ao
cliente RabbitMQ (
Net::AMQP::RabbitMQ) do Guia 8/ADR-014 — mas não toda exceção de plataforma: o Minion (ADR-008, não afetado por esta ADR) tem uma limitação própria e pré-existente defork()em Windows nativo, não relacionada a filas nem à dependência XS eliminada aqui — ver "Revisão 2026-07-08" acima - Observabilidade de fila via SQL puro (
pgque.get_queue_info()etc.), sem ferramenta externa (Management UI do RabbitMQ deixa de ser necessária) - Reaproveita um padrão já validado em produção no próprio stack (
Minion::Backend::Pgjá usaSKIP LOCKED/LISTEN/NOTIFY) em vez de introduzir um paradigma novo
Negativo:
- PgQue está em estágio inicial (v0.2.0, poucos meses de existência como projeto empacotado) — risco de maturidade real, ainda que o algoritmo subjacente (PgQ) tenha uma década de uso em produção na Skype
- Concentra mais carga de trabalho (filas, além de dados relacionais e JSONB) na
mesma instância PostgreSQL — risco de conflito de vacuum/WAL entre cargas sob
volume alto (ver seção 8 do estudo anexo). Mitigado pela ADR-023 (proposta
separada, também
Proposta): instânciadb-eventsdedicada, isolada dedb-app - Perde interoperabilidade AMQP nativa — se um sistema externo não-Perl precisar consumir esses eventos diretamente via protocolo padrão de mensageria no futuro, precisaria de uma ponte adicional (nada no stack usa isso hoje)
- Modelo de fan-out do PgQue (log de eventos com cursor por consumidor) é conceitualmente diferente de exchanges/routing keys do RabbitMQ — não é um substituto 1:1, é um modelo diferente que cobre o mesmo caso de uso
Ações necessárias (executadas na aceitação desta ADR, 2026-07-07):
- Pré-requisito: a baseline de
testes contra o mecanismo atual (
t/070_notifications.t, cobrindo os 3 jobs Minion sem teste antes e o roteamento doNotificationWorker) já existe no repositório da Stega — ver seção 10 do estudo anexo, "Cobertura de testes". Ao implementar esta ADR, a função de dreno desses testes (hoje conectando viaNet::AMQP::RabbitMQ) precisa ser reescrita por completo parapgque.receive()— as asserções sobre o resultado (routing key + payload) permanecem as mesmas e servem de critério de aceite - Atualizar o status da ADR-008 para
Substituída por ADR-022 - Remover o serviço
rabbitmqdocompose.yml(repositório central e Stega), incluindo do serviçotest(hoje depende derabbitmq/usaRABBITMQ_HOSTpara rodart/070_notifications.t— passa a depender depostgres-eventse usarPOSTGRESQL_EVENTS_URL/_USERNAME/_PASSWORD, ver ADR-023), e a instalação delibrabbitmq-dev/librabbitmq4do Dockerfile - Instalar
pgque.sqlno banco da Stega — o próprio script cria os papéispgque_reader/pgque_writer/pgque_adminde forma idempotente; o que o bootstrap acrescenta é a concessão depgque_adminà credencial que se conecta a esse banco - Reescrever
Stega::Worker::NotificationWorkere os jobs Minion que publicam eventos (SendWelcomeNotification,CheckSlaBreaches,GenerateActivityReport) para usarpgque.send()/receive()/ack()/nack()viaMojo::Pg - Remover
Net::AMQP::RabbitMQdocpanfile - Remover as variáveis
RABBITMQ_HOST,RABBITMQ_USER,RABBITMQ_PASSWORD,RABBITMQ_VHOSTeRABBITMQ_PORT— nenhuma substituta é necessária, já que o PgQue é acessado pela mesma conexão Postgres da aplicação (POSTGRESQL_APP_URLe credenciais associadas — formato da Revisão 2026-07-04 da ADR-016; a Stega hoje ainda usa o formato antigoPOSTGRESQL_URL, ver o pré-requisito equivalente nas Ações da ADR-023) ou, se a ADR-023 também for aceita, porPOSTGRESQL_EVENTS_URL/_USERNAME/_PASSWORD— não por um protocolo próprio com host/usuário/senha separados - Atualizar a lista de variáveis de ambiente documentada na ADR-021
(
Stega::Config) — remove a chaverabbitmqdo hashref de configuração - Criar o processo/
Deploymentde ticker (pgque.ticker()em loop, maismaint()/maint_retry_events()a ~30s emaint_rotate_tables_step2()a ~10s em transação própria — ver "Tick de rotação" na Decisão) no Kubernetes e o equivalente em Docker Compose para desenvolvimento - Reescrever o Guia 8 e atualizar os manifests Kubernetes do Guia 9 (remover
Deployment/Servicedo RabbitMQ, adicionar oDeploymentde ticker) - Remover a exceção "Windows nativo" do Guia 8 e da ADR-014
- Atualizar
docs/references/minion.md,mojo-pg.mdepostgresql.mdcom esta ADR na seção "Referenciada em" - Atualizar a tabela "Technology Stack" e "Decisões Iniciais Resolvidas" do
CLAUDE.md