Durable Delays: gerenciando o tempo como estado distribuído

SchemaBridge Team · 2026-01-10 · Time, Scheduling, Orchestration

Esperas de 30 dias sem vazamento de memória. Por que `sleep()` é um antipadrão em sistemas distribuídos.

A mentira do sleep(): por que o tempo é a primitiva distribuída mais difícil

Toda linguagem de programação tem um comando para esperar. Em Python, é time.sleep(). Em Node.js, é setTimeout(). Em Java, é Thread.sleep(). Esses comandos são simples, intuitivos e, para qualquer coisa além de alguns segundos, totalmente inúteis em um sistema distribuído em produção.

O humilde sleep() é uma mentira porque pressupõe que o ambiente é estável. Pressupõe que a máquina que executa o código continuará viva, que o processo não será encerrado por um load balancer e que a memória não será recuperada pelo sistema operacional. Em um ambiente de nuvem moderno, essas premissas são falsas. Se você fizer sleep(24 60 60) (um dia) em um pod Kubernetes comum, é muito provável que esse pod seja rotacionado, reagendado ou reimplantado antes de o dia acabar.

Quando o processo morre, seu "sleep" morre junto. Sua lógica de negócio se perde no vazio. É assim que nasce o "software esquecido" — sistemas que perdem o controle dos períodos de teste dos clientes, deixam passar renovações de assinatura e não enviam e-mails de acompanhamento críticos.

A hierarquia do tempo: de segundos a meses

Para gerenciar o tempo corretamente, primeiro precisamos categorizá-lo. Nem todo delay é igual:

1. Delays transitórios (milissegundos a segundos): Geralmente são backoffs de rede ou esperas por um lock rápido de banco de dados. Aqui o sleep() às vezes é aceitável, porque o risco de uma falha em uma janela de 50 ms é baixo.

2. Delays curtos (minutos): É aqui que o sleep() começa a falhar. Você ocupa uma thread de worker ou os recursos de um contêiner por minutos sem fazer nada. É desperdício de dinheiro e um risco para o pool de conexões.

3. Durable delays (horas a meses): Este é o domínio dos ciclos de vida de negócio. Um teste gratuito de 14 dias, um prazo de pagamento de 30 dias ou um cronograma de manutenção semestral. Isso não pode viver no código; precisa viver na infraestrutura.

A arquitetura dos workflows adormecidos

Na SchemaBridge, tratamos o tempo como estado durável. Quando seu workflow chega a um vértice DELAY, ele não bloqueia nenhuma thread. A etapa fica estacionada como SUSPENDED, a intenção de despertar é gravada no banco de dados e o worker fica imediatamente livre para outros trabalhos.

O modelo de polling no banco (baixa precisão, alta durabilidade)

A abordagem caseira clássica armazena o "horário de despertar" em uma tabela do banco, e um worker em segundo plano (o "poller") consulta a tabela a cada poucos segundos: SELECT * FROM timers WHERE wake_up < NOW(). Isso é durável porque um registro no banco sobrevive a reinicializações — mas você paga com um fluxo constante de consultas vazias e um piso de precisão igual ao seu intervalo de polling.

A abordagem da SchemaBridge

A SchemaBridge mantém a durabilidade do registro no banco e abandona o polling fixo. Cada despertar é persistido como uma linha com um execute_at absoluto em uma tabela scheduled_tasks do PostgreSQL (por trás do nosso ScheduledTaskRepository). O clock worker dorme até o execute_at pendente mais próximo e é acordado antes por um push de LISTEN/NOTIFY do PostgreSQL sempre que uma tarefa se torna executável. Quando há tarefas vencidas, ele as reivindica em uma única instrução com FOR UPDATE SKIP LOCKED, para que várias réplicas do clock worker possam dividir o backlog sem colisões. Um teto de ociosidade folgado existe apenas como rede de segurança caso uma notificação seja perdida.

-- Formato simplificado do claim: linhas vencidas, as mais antigas primeiro, particionadas entre réplicas
UPDATE scheduled_tasks SET status = 'PROCESSING'
WHERE task_id IN (
  SELECT task_id FROM scheduled_tasks
  WHERE status = 'PENDING' AND execute_at <= :now
  ORDER BY execute_at
  FOR UPDATE SKIP LOCKED
  LIMIT 100
)
RETURNING task_id;

Cancelamento e saídas antecipadas: mudando o futuro

Um durable delay é só metade da história. Se um usuário está em uma etapa "Aguardar 3 dias pelo pagamento" e paga depois de 2 horas, o que deve acontecer?

Primeiro, o cancelamento. Parar uma execução na SchemaBridge cancela a execução e, em seguida, desarma os timers de delay e de aprovação pendentes dessa execução, de modo que uma jornada interrompida não acorde dias depois e continue. A ordem é deliberada — primeiro cancelar, depois desarmar —, então o pior caso é um timer disparando em uma execução já cancelada, onde as proteções de estado terminal o tornam um no-op inofensivo.

Segundo, o despertar antecipado. A SchemaBridge não tem uma primitiva nativa do tipo "acorde este delay quando chegar um evento externo". Um vértice DELAY espera a duração configurada. Há duas formas honestas de modelar uma saída antecipada:

Nenhum dos dois padrões exige que o motor coloque um timer para competir com um sinal — exatamente o tipo de condição de corrida que você quer eliminar por design, em vez de confiar que não vai acontecer.

Clock drift e precisão

Em qualquer sistema distribuído, os relógios nunca estão perfeitamente sincronizados. Esse é o fenômeno do clock drift (deriva de relógio). Se o relógio de uma máquina está 50 ms adiantado em relação ao de outra, um "Aguardar 1 segundo" pode se comportar de forma diferente dependendo de onde a tarefa é capturada.

A SchemaBridge mantém o problema pequeno fazendo do despertar um timestamp absoluto em um único lugar: a coluna execute_at no PostgreSQL. Armar um delay calcula now + delaySeconds uma única vez e o armazena; a partir daí, nenhum worker mantém uma contagem regressiva em memória, então não existe timer por worker que possa derivar ou se perder em uma reinicialização. A diferença comum de relógio entre máquinas continua existindo, mas, para delays de negócio medidos em horas ou dias, ela é irrelevante para o resultado.

Cenário ilustrativo: automatizando uma campanha drip de 30 dias

Considere uma plataforma de automação de marketing com dificuldades na lógica da sua "Welcome Journey".

O desafio

Uma jornada envolve:

Construídas sobre uma solução própria com cron jobs e uma tabela no Postgres, jornadas assim são frágeis: durante uma janela de manutenção do banco de dados ou um deploy, uma parcela dos usuários pode ficar "presa" em um estado de espera e nunca receber o próximo e-mail.

O jeito SchemaBridge

Imagine as mesmas jornadas reconstruídas como workflows da SchemaBridge.

1. Delays visuais: Você arrasta um vértice DELAY para o grafo e define delaySeconds como 259200 (3 dias) e 604800 (7 dias). O editor os exibe como durações compactas, como "3d".

2. Retomada durável: Durante os deploys, as execuções em espera simplesmente continuam estacionadas no banco de dados. Quando o clock worker volta, ele reivindica os timers que venceram durante a indisponibilidade, os mais antigos primeiro, e os retoma.

3. Reverificar ao despertar: Após cada delay, uma etapa Gateway pergunta ao provedor de e-mail ou de cobrança se o usuário clicou ou fez upgrade, e uma aresta condicional escolhe a próxima etapa.

O retorno esperado

Comparação: estratégias de agendamento existentes

| Recurso | setTimeout() | Cron jobs / Quartz | Durable Delays da SchemaBridge |

| :--- | :--- | :--- | :--- |

| Persistência | Nenhuma (volátil) | Manual (apoiada em banco) | Nativa (PostgreSQL scheduled_tasks) |

| Escalabilidade | Baixa (presa à RAM) | Média (gargalo no banco) | Alta (SKIP LOCKED entre réplicas) |

| Cancelamento | Complexo (handle manual) | Limpeza manual no banco | Parar uma execução desarma seus timers |

| Visibilidade | Nenhuma | Consultas SQL | Etapas estacionadas (suspensas) visíveis na visão da execução |

| Resolução | Milissegundos | Segundos/minutos | Despertar por push (LISTEN/NOTIFY), sem polling fixo |

Checklist de especialista para lógica de negócio de longa duração

Se você está projetando um sistema que espera mais de 5 minutos, siga estas heurísticas:

1. Libere a thread: Nunca bloqueie um worker enquanto espera. Seu worker deve ser stateless e estar pronto para ser encerrado a qualquer momento.

2. Externalize o relógio: Use um armazenamento central e durável para guardar o horário de despertar, não o timer em memória da máquina local.

3. Projete para resultados antecipados: Sempre assuma que o usuário pode realizar a ação que você está esperando antes de o delay expirar. Reverifique o estado quando a espera terminar, em vez de presumir que nada mudou.

4. Audite a sala de espera: Acompanhe quantas execuções têm etapas estacionadas em um temporizador. Esse é um indicador importante da saúde do negócio.

5. Prefira delays relativos: Expresse uma espera como uma duração a partir da etapa anterior (delaySeconds), e não como uma data fixa no calendário, para que, se uma etapa anterior atrasar, o intervalo relativo entre as etapas permaneça consistente.

Conclusão: tempo é um problema de infraestrutura

Software que esquece é software que falha. Ao tratar o tempo como um estado durável de primeira classe, construímos a ponte entre eventos em tempo real e o comportamento humano de alta latência. Com a SchemaBridge, uma jornada que dura dias ou semanas sobrevive às reinicializações e aos deploys que matariam uma thread adormecida — desde que você projete as saídas antecipadas e carregue adiante os dados de que as etapas posteriores precisam.

A capacidade de gerenciar com confiabilidade o timing das suas interações é uma vantagem competitiva real. Nós fornecemos o relógio durável; você fornece a jornada.

Parte da nossa série Building the Bridge. Veja também: A lacuna de observabilidade para depurar execuções longas e com várias etapas sem se perder nos logs.

Explorar