SaaS Starter

Enfileirar um background job

Declarar uma queue, enfileirar trabalho, escrever um worker handler, monitorar com Bull Board.

Background jobs usam BullMQ sobre Redis. O catálogo de queues é um set fechado declarado em código para que produtores e consumidores não possam discordar sobre um nome ou forma de payload.

apps/server/src/infrastructure/jobs/queues.ts:

export const QUEUE_NAMES = {
  emails: 'emails',
  webhooksOutgoing: 'webhooks-outgoing',
  billingWebhooksRetry: 'billing-webhooks-retry',
  accountDeletions: 'account-deletions',
  dataExports: 'data-exports',
  usagePeriodRollover: 'usage-period-rollover',
} as const;

export interface JobDataMap {
  emails: EmailJobData;
  'webhooks-outgoing': WebhooksOutgoingJobData;
  'billing-webhooks-retry': BillingWebhooksRetryJobData;
  'account-deletions': AccountDeletionJobData;
  'data-exports': DataExportJobData;
  'usage-period-rollover': UsagePeriodRolloverJobData;
}

Um nome de queue digitado errado falha em compile time. Uma forma de payload errada falha em compile time.

[!NOTE] usage-period-rollover é instalado como tick recorrente (hourly @ :05 UTC) via schedulePeriodRollover() no startup do server. O payload vai vazio — o processor lê o tempo do tick de job.processedOn porque BullMQ persiste o payload dos jobs recorrentes no momento do schedule. Ver Usage metering para o ciclo de vida completo.

Política de retry padrão

apps/server/src/infrastructure/jobs/queue-factory.ts define as opções default por job:

export const DEFAULT_JOB_OPTIONS: JobsOptions = {
  attempts: 3,
  backoff: { type: 'exponential', delay: 5_000 },
  removeOnComplete: { age: 60 * 60 * 24, count: 1_000 },
  removeOnFail: { age: 60 * 60 * 24 * 7 },
};

3 tentativas com backoff exponencial ancorado em 5 s, jobs completos varridos após 24 h, jobs falhos mantidos por 7 dias. Produtores podem fazer override por chamada.

1. Adicionar uma nova queue

// apps/server/src/infrastructure/jobs/queues.ts
export const QUEUE_NAMES = {
  /* ... */,
  reportExports: 'report-exports',
} as const;

export interface ReportExportJobData {
  readonly userId: string;
  readonly reportId: string;
  readonly format: 'csv' | 'pdf';
}

export interface JobDataMap {
  /* ... */,
  'report-exports': ReportExportJobData;
}

A queue é auto-registrada para iteração no Bull Board.

2. Escrever o worker handler

apps/server/src/infrastructure/jobs/processors/report-exports.processor.ts:

import type { Processor } from 'bullmq';
import type { ReportExportJobData } from '../queues.js';

export const reportExportsProcessor =
  (deps: { logger: Logger; reports: ReportsService }): Processor<ReportExportJobData> =>
  async (job) => {
    deps.logger.info({ jobId: job.id }, 'export starting');
    await deps.reports.export(job.data);
  };

Cabeie em bootstrap/worker.ts para que o worker suba com esse handler anexado. O processo do worker e o processo da API compartilham a queue mas rodam independentemente — a API enfileira, o worker drena.

3. Enfileirar de um use case ou listener

await deps.jobs.enqueue('report-exports', {
  userId: input.userId,
  reportId: input.reportId,
  format: 'pdf',
});

jobs é o port JobScheduler (veja infrastructure/jobs/bullmq-job-scheduler.adapter.ts). Enfileirar a partir de um listener de evento de domínio é o padrão mais comum.

Para fazer override de retry options numa chamada específica:

await deps.jobs.enqueue('report-exports', payload, { attempts: 5, delay: 30_000 });

4. Idempotência

Workers tentam de novo. Handlers precisam ser idempotentes. Dois padrões:

  • Operação externa determinística: inclua uma key que o terceiro consiga deduplicar (Stripe Idempotency-Key, uma linha de outbox keyada no job id, etc.).
  • Escrita interna: gate numa coluna "processed at" ou num unique index que absorva o segundo insert.

A queue de retry de webhooks de billing do boilerplate usa o id do evento do provider como idempotency key — replays são no-op.

5. Monitorar

Bull Board é montado em /admin/queues quando BULL_BOARD_ENABLED=true (default em dev, off em prod). É protegido — você precisa de uma session autenticada com grants de admin para alcançar.

Counts e profundidade por queue são enviados ao Prometheus via o endpoint /metrics. Alerte em bullmq_failed_jobs_total subindo ou bullmq_waiting_jobs ficando non-zero por mais tempo do que o esperado.

Testing

Faça unit-test do wrapper (o use case que chama jobs.enqueue) com um JobScheduler fake que grava chamadas. Testes end-to-end de BullMQ precisam de um Redis real — BullMQ usa Lua + cmsgpack que ioredis-mock não consegue rodar. Teste a lógica do handler isoladamente; reserve o end-to-end completo contra Redis para o pequeno conjunto de jobs cuja interação com Redis é a coisa sob teste.

Nesta página