O worker A pega um lock por 10 segundos e começa a recalcular um documento. Ele trava por 12 segundos. O lock expira, o worker B assume, grava o resultado novo, e o A volta à vida e sobrescreve tudo. Embora o serviço de lock tenha funcionado como prometido, a implementação esconde um detalhe crucial: o lock distribuído com TTL não é um mutex. É um lease, uma posse temporária. Quando o tempo acaba, o coordenador pode entregar a posse a outro worker, mas não consegue arrancar o código antigo da CPU nem cancelar um pacote que ficou preso na rede.

Seu lock expirou — e o worker antigo ainda escreveu

Timeout Venceu, Mas o Dono Antigo Não Morreu

O cenário pode ser GC, CPU disputada, page fault, container congelado ou um SIGSTOP acidental. Também pode nem estar no processo: o worker envia a requisição dentro do prazo, mas ela chega ao banco depois. Em um incidente do GitHub de 2012, o tráfego entre switches ficou bloqueado por cerca de 90 segundos. Pares de servidores de arquivos perderam heartbeats, comandos de isolamento não chegaram e, quando a rede voltou, vários pares tinham os dois nós esperando ser o ativo do mesmo recurso. O risco declarado era corrupção irrecuperável; restaurar toda a infraestrutura afetada levou mais de cinco horas.

Saída Não É Aumentar o TTL

A saída não é aumentar o TTL. É fazer o recurso protegido rejeitar o dono antigo com um fencing token. O timeout venceu, mas o dono antigo não morreu.

Implementação Comum

Quem já estudou o padrão do Redis talvez encontre um erro no release: ele precisa apagar a chave somente se o valor ainda for o mesmo valor aleatório criado na aquisição. Correto. Sem essa comparação, o worker A pode acordar depois do vencimento e apagar o lock novo do B. A documentação de locks distribuídos do Redis usa SET chave valor-aleatorio NX PX ttl; no Redis 8.4 existe DELEX. IFEQ, e nas versões anteriores a comparação costuma ser feita por Lua.

Só que isso protege a remoção do lock. O salvar continua aberto. O valor aleatório responde 'este ainda é o mesmo lock que eu peguei?'. Ele não responde 'esta operação é mais nova que todas as operações aceitas antes?'. Checar o lock imediatamente antes de gravar também não fecha o buraco. O processo pode pausar entre a checagem e a gravação. A requisição pode sair logo depois da checagem e ficar atrasada no caminho.

Validar e Mutar em Lugar Diferente

Enquanto a validação e a mutação acontecem em lugares diferentes, sobra uma janela. O banco precisa poder dizer 'você é velho'. Na aquisição, o coordenador entrega uma geração estritamente crescente: toda gravação leva essa geração. O recurso guarda o maior número já aceito e recusa qualquer número menor.

Chubby do Google

O paper do Chubby, publicado pelo Google em 2006, descreve quatro números monotônicos de 64 bits por nó. Um deles é a geração do lock, incrementada quando o lock passa de livre para ocupado. O cliente podia pedir um sequencer: uma sequência opaca contendo nome, modo e geração do lock. Depois enviava esse valor ao file server ou a outro serviço protegido. O destinatário validava o sequencer ou comparava com o mais recente que já tinha visto.

Essa parte é reveladora: o Chubby não tentava transformar o lock em uma barreira física. Os locks eram consultivos. A segurança nascia quando o serviço que recebia a operação conferia a geração.

Para sistemas antigos que não entendiam sequencers, o Chubby tinha um lock-delay: após uma falha, esperava até um minuto antes de liberar o lock novamente.

Conclusão

Implementações de lock distribuídos com TTL precisam garantir que o recurso protegido rejeite donos antigos usando tokens de fencing. Isso é crucial para evitar corridas e garantir que o sistema funcione corretamente em situações de atraso na rede e nos processos.

AindaAntigoExpirouLockWorker