Pular para o conteúdo

ADR-0028 Jobs em segundo plano e agendamento

Padrão da empresa para trabalho fora do pedido — filas e job schedulers do BullMQ sobre o Valkey que já corremos, processados por um contentor worker da mesma imagem.

Decisão
BullMQ sobre ValkeyConstruir a app
Estado
Aceite
Data
Âmbito
Predefinição da empresa para todas as novas apps

Há trabalho que não deve correr dentro de um pedido: enviar email, chamar um terceiro lento, gerar uma exportação, tudo o que deve ser repetido quando falha. Outro trabalho corre a horas certas: limpezas, resumos, sincronizações.

  • A app é um processo Bun em Docker (ADR-0003), substituído em cada deploy e por vezes a correr em várias réplicas. Um temporizador dentro dela dispara uma vez por réplica e morre com o contentor
  • O Valkey já corre ao lado de cada app para os limites de pedidos e as sessões, e o ADR-0010 já contava com filas sobre ele
  • O Postgres é a fonte de verdade (ADR-0007), e o ADR-0010 afastou dele, de propósito, as escritas de alta rotação

O que está em aberto: a biblioteca de filas, onde vive o seu estado e onde correm os jobs.

Prós

  • Filas, repetições com backoff, jobs adiados, concorrência e Job Schedulers ao estilo cron numa só biblioteca MIT
  • A documentação indica o Valkey como servidor suportado
  • Corre sobre ioredis, o cliente que já usamos (peer dependency desde o BullMQ 6.0)
  • O bullmq-otel liga a colocação em fila e o processamento de um job num só trace (ADR-0015)

Contras

  • Precisa de um Valkey que persista e nunca remova chaves — o ADR-0010 corria-o sem persistência
  • A entrega é pelo menos uma vez: todos os handlers têm de poder correr duas vezes sem dano
  • Colocar um job em fila não faz parte de uma transação Postgres

2. Uma fila sobre Postgres (pg-boss, ou o backend PostgreSQL do BullMQ)

Seção intitulada “2. Uma fila sobre Postgres (pg-boss, ou o backend PostgreSQL do BullMQ)”

Prós

  • Um job pode ser colocado em fila na mesma transação da escrita de negócio
  • Nenhuma alteração ao Valkey; os jobs ficam cobertos pelo backup da base de dados

Contras

  • Põe a rotação das filas no Postgres — a carga que o ADR-0010 manteve de fora
  • O backend Redis do BullMQ é o predefinido; o PostgreSQL é o caminho mais recente

3. Temporizadores no processo web (setInterval, node-cron)

Seção intitulada “3. Temporizadores no processo web (setInterval, node-cron)”

Prós: nada para operar. Contras: dispara uma vez por réplica; perde-se num reinício ou deploy; sem repetições, sem backoff, sem registo do que falhou.

BullMQ sobre Valkey é o padrão da empresa para jobs em segundo plano e trabalho agendado.

Em concreto:

  • Pacotes: bullmq 6.x (6.7.6 à data de escrita) com ioredis 5.x instalado explicitamente — a mesma major a que o ADR-0009 nos obriga
  • Valkey: a instância do ADR-0010, agora com persistência AOF (fsync a cada segundo) e maxmemory-policy noeviction. O guia de produção do BullMQ diz que noeviction é a única definição que garante filas corretas
  • Duas ligações, nenhuma delas a do limitador. As filas no processo web ligam-se com enableOfflineQueue: false, para que colocar um job em fila falhe depressa quando o Valkey está em baixo, em vez de prender o pedido. Os workers ligam-se com maxRetriesPerRequest: null, que o BullMQ exige — no cliente do limitador, essa definição faria as verificações de limite esperar em vez de recorrer ao insuranceLimiter
  • Os jobs correm num contentor worker, nunca no processo web: um segundo entry point da apps/web na mesma imagem, tal como o entry point de migrações do ADR-0016. O processo web só coloca em fila. Só passa a ser uma apps/worker/ própria (ADR-0026) quando precisar das suas próprias dependências ou imagem
  • Um módulo por fila, em server/jobs/: o nome, um schema Zod para o payload (ADR-0005) e o handler. O payload é validado ao colocar em fila e outra vez no worker — durante um deploy pode ter sido escrito por uma versão antiga. Os payloads levam IDs, não registos inteiros
  • Os handlers são idempotentes. Um job parado ou repetido volta a correr
  • Todas as filas definem attempts com backoff exponencial, e removeOnComplete / removeOnFail com um número ou uma idade — um histórico de jobs sem limite enche um store que não pode remover chaves
  • Agendar é queue.upsertJobScheduler(id, { pattern }), chamado quando o worker arranca. É um upsert, pelo que réplicas e novos deploys não duplicam um agendamento. Nada de node-cron nem de setInterval para trabalho recorrente
  • Colocar em fila depois do commit no Postgres, e desenhar o job de modo a que perdê-lo ou repeti-lo seja seguro
  • Encerramento: em SIGTERM o worker chama worker.close() e sai dentro do período de tolerância; um job que não termine fica marcado como parado e é retomado
  • Telemetria: bullmq-otel 2.x passado como telemetry às filas e aos workers, com enableMetrics ligado, para que os traces dos jobs e as métricas das filas saiam pelo endpoint OTLP da app (ADR-0015)
  • Testes: os testes de integração correm workers reais contra o contentor Valkey do ADR-0014

Os fatores decisivos: nenhuma infraestrutura nova, uma só biblioteca para filas e agendamentos, o cliente Redis que já usamos e a carga das filas fora do Postgres. Aceitamos ligar a persistência no Valkey e escrever handlers idempotentes.

Mais fácil:

  • O trabalho lento e repetível sai do caminho do pedido; um envio falhado é repetido em vez de perdido
  • Os agendamentos vivem no código, são entregues com a app e correm uma só vez, haja as réplicas que houver
  • As ligações de verificação pendentes e o estado do OAuth (ADR-0009) passam a sobreviver a um reinício do Valkey

Mais difícil:

  • O Valkey precisa de um volume e de um limite de memória com folga. Filas, sessões e limites de pedidos partilham-no: uma fila que encha a memória faz falhar as escritas das sessões e do limitador
  • O Nodus tem de correr o entry point do worker como segundo contentor — ainda não faz parte do contrato do ADR-0016
  • O AOF protege de um reinício, não da perda da máquina; se o conteúdo das filas precisa de backup decide-se por deployment (ADR-0032)
  • A idempotência é um hábito, garantido na revisão

Do que abdicamos: de colocar um job em fila na mesma transação de uma escrita na base de dados. Aceite — um só store para filas, contadores e sessões compensa desenhar jobs que se podem perder ou repetir sem dano.