Introdução ao Temporal.io para workflows de longa duração em produção

1. O que é o Temporal.io e por que ele existe?

1.1. Problemas clássicos de workflows de longa duração

Workflows de longa duração são comuns em sistemas modernos: onboarding de usuários, processamento de pedidos, pipelines de dados, orquestração de microsserviços. Esses workflows enfrentam desafios clássicos:

  • Timeouts: operações que demoram horas ou dias podem expirar antes da conclusão
  • Retentativas: falhas temporárias exigem repetição, mas sem lógica complexa manual
  • Estado distribuído: manter o estado consistente entre múltiplos serviços é difícil
  • Coordenação: sincronizar etapas assíncronas sem deadlocks ou perda de dados

Soluções tradicionais como filas simples (RabbitMQ, SQS) ou orquestradores manuais (código com estados em banco) resolvem partes do problema, mas introduzem complexidade adicional.

1.2. Conceitos fundamentais

Temporal.io resolve esses problemas com três conceitos principais:

  • Workflows: código que define a lógica de orquestração, escrito como uma função que pode ser pausada e retomada
  • Activities: funções que executam a lógica de negócio real (chamadas HTTP, acesso a banco, envio de emails)
  • Workers: processos que executam Workflows e Activities, mantendo o estado no Temporal Server

1.3. Diferença para soluções tradicionais

Diferente de filas (que são fire-and-forget) ou sagas manuais (que exigem código de compensação em cada serviço), Temporal oferece:

  • Execução reentrante: workflows podem ser pausados e retomados exatamente de onde pararam
  • Garantia de execução: mesmo que o worker morra, o workflow continua em outro worker
  • Estado centralizado: o Temporal Server mantém o estado completo do workflow

2. Arquitetura e componentes principais

2.1. Temporal Server e armazenamento persistente

O Temporal Server é o cérebro do sistema. Ele armazena:

  • Histórico completo de eventos de cada workflow
  • Estado atual de execução
  • Filas de tasks para Workers

O armazenamento pode usar Cassandra, PostgreSQL ou MySQL. Para produção, recomenda-se Cassandra ou PostgreSQL com replicação.

2.2. Workers: execução reentrante e replay

Workers são processos que executam Workflows e Activities. A mágica está no replay: quando um workflow é retomado, o Worker reexecuta o código do workflow a partir do histórico de eventos, não da lógica real. Isso garante:

  • Determinismo: o mesmo histórico sempre produz o mesmo resultado
  • Resiliência: falhas de worker não perdem progresso
  • Escalabilidade: múltiplos workers podem processar workflows simultaneamente

2.3. Client SDK e comunicação assíncrona

O Client SDK (disponível para Go, Java, Python, TypeScript, .NET) permite iniciar workflows, enviar sinais e consultar estado. A comunicação com o Server é assíncrona e baseada em gRPC.

3. Modelando um workflow de longa duração na prática

3.1. Estrutura básica de um Workflow

Um workflow em TypeScript se parece com:

import { proxyActivities, sleep, defineQuery, setHandler } from '@temporalio/workflow';
import type * as activities from './activities';

const { sendEmail, createUserInDB, validateDocument } = proxyActivities<typeof activities>({
  startToCloseTimeout: '30 seconds',
  retry: { maximumAttempts: 3 },
});

export async function onboardingWorkflow(userId: string, email: string): Promise<string> {
  // Define query para consultar estado
  const statusQuery = defineQuery<string>('getStatus');
  let status = 'INICIADO';

  setHandler(statusQuery, () => status);

  // Etapa 1: Validação de documento
  status = 'VALIDANDO_DOCUMENTO';
  const documentValid = await validateDocument(userId);

  if (!documentValid) {
    status = 'DOCUMENTO_INVALIDO';
    await sendEmail(email, 'Documento inválido. Por favor, reenvie.');
    return 'REJEITADO';
  }

  // Etapa 2: Criação no banco
  status = 'CRIANDO_USUARIO';
  await createUserInDB(userId);

  // Etapa 3: Aguarda aprovação manual (pode levar horas)
  status = 'AGUARDANDO_APROVACAO';
  await sleep('24h'); // Temporizador de longa duração

  status = 'APROVADO';
  await sendEmail(email, 'Seu cadastro foi aprovado!');
  return 'APROVADO';
}

3.2. Activities: lógica de negócio com retentativas

Activities são funções que executam trabalho real:

// activities.ts
export async function validateDocument(userId: string): Promise<boolean> {
  const response = await fetch(`https://api.documents.com/validate/${userId}`);
  if (!response.ok) throw new Error('Falha na validação');
  return response.json();
}

export async function createUserInDB(userId: string): Promise<void> {
  await db.users.create({ id: userId, status: 'ACTIVE' });
}

export async function sendEmail(to: string, body: string): Promise<void> {
  await emailService.send({ to, subject: 'Atualização de cadastro', body });
}

3.3. Exemplo completo: workflow de onboarding

Iniciando o workflow:

import { WorkflowClient } from '@temporalio/client';
import { onboardingWorkflow } from './workflows';

const client = new WorkflowClient();

async function startOnboarding(userId: string, email: string) {
  const handle = await client.start(onboardingWorkflow, {
    args: [userId, email],
    taskQueue: 'onboarding-tasks',
    workflowId: `onboarding-${userId}`,
  });

  console.log(`Workflow iniciado: ${handle.workflowId}`);
  return handle;
}

4. Lidando com falhas e resiliência em produção

4.1. Retentativas automáticas com backoff exponencial

Temporal gerencia retentativas automaticamente. Você configura:

const { processPayment } = proxyActivities<typeof activities>({
  startToCloseTimeout: '1 minute',
  retry: {
    initialInterval: '1 second',
    maximumInterval: '1 minute',
    backoffCoefficient: 2,
    maximumAttempts: 5,
    nonRetryableErrorTypes: ['PermanentError'],
  },
});

4.2. Garantia de execução exactly-once

Temporal garante que cada Activity execute pelo menos uma vez, mas com side effects idempotentes. O workflow só avança após confirmação do servidor. Se um worker morre durante uma Activity, outro worker retoma a partir do último evento confirmado.

4.3. Estratégias de compensação

Para workflows que precisam de rollback, use um padrão de Saga:

export async function orderWorkflow(orderId: string) {
  const steps = [
    { name: 'reserveInventory', compensate: 'releaseInventory' },
    { name: 'chargePayment', compensate: 'refundPayment' },
    { name: 'shipOrder', compensate: 'cancelShipment' },
  ];

  const executedSteps = [];

  try {
    for (const step of steps) {
      await activities[step.name](orderId);
      executedSteps.push(step);
    }
  } catch (error) {
    // Compensar na ordem reversa
    for (const step of executedSteps.reverse()) {
      await activities[step.compensate](orderId);
    }
    throw error;
  }
}

5. Técnicas avançadas: sinais, queries e cron jobs

5.1. Sinais: comunicação externa

Sinais permitem modificar workflows em execução:

// Workflow
export async function approvalWorkflow(userId: string) {
  let approved = false;

  setHandler(defineSignal('approve'), () => { approved = true; });
  setHandler(defineSignal('reject'), () => { approved = false; });

  await condition(() => approved !== undefined, '48h');

  if (approved) {
    await sendEmail(userId, 'Aprovado!');
  }
}

// Client
await handle.signal('approve');

5.2. Queries: consulta de estado

Queries não modificam o workflow:

// Client
const status = await handle.query('getStatus');
console.log(`Status atual: ${status}`);

5.3. Temporizadores e cron schedules

export async function dailyReportWorkflow() {
  while (true) {
    await generateReport();
    await sleep('24h'); // Executa todo dia
  }
}

// Ou usando cron nativo
await client.start(dailyReportWorkflow, {
  cronSchedule: '0 8 * * *', // Todo dia às 8h
});

6. Boas práticas de implantação e operação

6.1. Configuração de Workers

// worker.ts
import { Worker } from '@temporalio/worker';

async function run() {
  const worker = await Worker.create({
    workflowsPath: require.resolve('./workflows'),
    activitiesPath: require.resolve('./activities'),
    taskQueue: 'onboarding-tasks',
    maxConcurrentActivityTaskExecutions: 100,
    maxConcurrentWorkflowTaskExecutions: 50,
  });

  await worker.run();
}

6.2. Versionamento de Workflows

Use versionamento para mudanças sem quebrar execuções em andamento:

// Versão 1
export async function onboardingWorkflowV1(userId: string) { ... }

// Versão 2
export async function onboardingWorkflowV2(userId: string) { ... }

// No worker
const worker = await Worker.create({
  workflowsPath: require.resolve('./workflows'),
  // Registra ambas versões
});

6.3. Monitoramento com OpenTelemetry

import { OpenTelemetrySdk } from '@temporalio/interceptors-opentelemetry';

const otel = new OpenTelemetrySdk({
  exporter: new OTLPTraceExporter(),
});

const worker = await Worker.create({
  interceptors: [otel.createWorkerInterceptor()],
});

7. Comparação com alternativas e quando usar Temporal

7.1. Temporal vs Bull/BullMQ

Bull/BullMQ é excelente para filas simples com jobs independentes. Temporal é superior quando:

  • Workflows têm múltiplas etapas com dependências complexas
  • É necessário estado compartilhado entre etapas
  • Workflows podem durar dias ou meses

7.2. Temporal vs Sagas manuais

Sagas manuais exigem lógica de compensação em cada serviço. Temporal centraliza a orquestração, reduzindo complexidade e garantindo consistência.

7.3. Critérios de decisão

Use Temporal quando:
- Workflows têm múltiplas etapas assíncronas com dependências
- É necessário resiliência a falhas em qualquer ponto
- Workflows podem durar horas, dias ou meses

Evite Temporal quando:
- Apenas filas simples são necessárias (use Bull/BullMQ)
- O sistema é extremamente simples com poucas etapas
- A latência de sub-milissegundo é crítica (Temporal adiciona alguma sobrecarga)

Referências