Retries são uma história que você conta a si mesmo sobre confiabilidade
Um retry cego não torna uma ação confiável, ele a faz acontecer duas vezes. Confiabilidade de verdade é uma idempotency key e uma checagem de que o efeito aterrissou, não reexecutar até nenhuma exception voltar.
Apollo Space Research
Apollo Space
Um agent é pedido para enviar uma fatura. Ele chama a tool, a rede dá um soluço na volta, e o agent nunca ouve a palavra “feito.” Então ele faz a coisa que parece sensata: tenta de novo. A segunda chamada tem sucesso. O agent reporta sucesso. O cliente recebe duas faturas.
Ninguém escreveu um bug. Cada linha fez exatamente o que foi dito. O retry, o movimento que buscamos para tornar um sistema mais confiável, é a coisa que acabou de cobrar de alguém duas vezes.
Um retry não torna uma ação confiável. Ele a faz acontecer de novo. Se “de novo” é seguro ou catastrófico depende inteiramente de uma propriedade da qual o retry não sabe nada.
Este post é sobre essa propriedade, por que agents tornam o problema mais agudo que qualquer sistema antes deles, e a disciplina de duas partes que transforma um retry de uma arma carregada numa ferramenta de confiabilidade de verdade.
O retry que mente para você
Aqui está o movimento que todo mundo escreve primeiro, porque parece boa engenharia óbvia. Envolva a chamada arriscada num loop. Se ela lançar, durma um momento e tente de novo. Desista depois de algumas tentativas. Coloque em produção.
attempt = 0
while attempt < 3:
try:
send_invoice(customer)
break
except:
attempt += 1
sleep(backoff)
Por muito tempo isso funciona, e essa é a armadilha, ele funciona exatamente para as operações onde não importava. Ler um registro. Buscar um preço. Perguntar a previsão do tempo. Rode essas duas vezes e o pior que você fez foi desperdiçar um pouco de tempo. O loop parece um padrão de confiabilidade porque as chamadas que ele primeiro encontrou eram inofensivas de repetir.
Então alguém envolve um write no mesmo loop, e o loop mantém a promessa dele do único jeito que sabe: fazendo a coisa de novo.
A mentira está escondida na palavra “falhou.” Quando send_invoice lança, o loop conclui que a fatura não foi enviada. Mas uma exception do seu lado não significa que nada aconteceu do outro lado. A request pode aterrissar, o trabalho pode completar, a fatura pode sair, e o acknowledgment pode morrer no caminho de casa. Para o loop, uma resposta perdida e uma falha de verdade parecem idênticas. Então ele dá retry numa coisa que já teve sucesso, e o cliente paga pela diferença.
Um retry não torna uma ação confiável. Ele a faz acontecer de novo. O loop ingênuo nunca fez a única pergunta que importa: fazer isso duas vezes é o mesmo que fazer uma vez?
A palavra para “seguro de repetir” é idempotente
Divida toda ação que seu sistema pode tomar em duas pilhas.
Na primeira pilha, fazê-la duas vezes é idêntico a fazê-la uma vez. Setar o status de um cliente para “active.” Armazenar um arquivo num path fixo. Dizer a um registro que o email dele é x. Rode qualquer uma dessas cem vezes e o estado final é o mesmo que rodá-la uma vez. Essas ações são idempotentes, e para elas o retry loop ingênuo está genuinamente bem. Repita à vontade.
Na segunda pilha, toda repetição é um novo evento no mundo. Enviar um email. Cobrar um cartão. Emitir uma fatura. Postar uma mensagem. Criar um ticket. Essas não convergem num estado, elas acumulam. Duas cobranças não são uma cobrança confirmada duas vezes; são duas cobranças. Para esta pilha, o retry loop não é uma rede de segurança. É um gerador de duplicatas com backoff exponencial.
Retries são de graça na primeira pilha e perigosos na segunda. O problema inteiro é que o loop não consegue dizer em qual pilha está.
O loop ingênuo trata as duas pilhas igualmente porque só olha um sinal: voltou uma exception? Esse sinal te diz sobre o seu lado do fio. Ele não te diz nada sobre se o efeito aconteceu do outro lado, e para a segunda pilha, o efeito é o ponto inteiro.
Então a correção não é “dê retry menos” ou “dê retry com mais cuidado.” Dar retry com cuidado ainda envia duas faturas. A correção é fazer a pilha perigosa se comportar como a primeira pilha, dar a uma ação não-idempotente uma memória, para que a segunda tentativa saiba que a primeira já contou.
Parte um: a idempotency key dá uma memória à ação
Aqui está a versão elegante, e a ideia-chave é simples: faça toda ação perigosa carregar um nome, e recuse fazer a mesma ação nomeada duas vezes.
Antes de o agent enviar a fatura, ele cunha um identificador estável para esta intenção específica, não um número aleatório novo a cada tentativa, mas um valor derivado do próprio trabalho: este cliente, este período de faturamento, esta fatura. Chame-o de idempotency key. A key viaja com toda tentativa daquele mesmo envio.
Agora a operação que faz o envio checa a key primeiro. Uma fatura já foi emitida sob esta key exata? Se sim, ela não envia uma segunda, ela retorna o resultado da primeira. Se não, ela envia, e registra a key enquanto faz isso, atomicamente, no mesmo fôlego que o efeito. A primeira tentativa escreve “esta key está feita.” Todo retry depois lê “esta key está feita” e retorna o resultado original em vez de produzir um novo.
O retry ainda acontece. O loop ainda dispara três vezes no escuro. Mas agora só a primeira tentativa tem qualquer efeito, porque as outras duas chegam carregando um nome que o sistema já viu. Não paramos de dar retry. Tornamos o retry seguro, movemos a pilha perigosa para a inofensiva, de propósito, com uma key.
Duas coisas fazem ou quebram isso, e ambas são fáceis de errar sutilmente.
A key tem que ser estável através das tentativas e única através das intenções. Se cada retry gera uma nova key, o sistema vê três requests diferentes e envia três faturas, você reconstruiu o bug original com passos extras. Se duas faturas genuinamente diferentes colidem na mesma key, a segunda silenciosamente nunca envia, e agora você perdeu uma de verdade. A key tem que ser uma impressão digital da intenção: mesma intenção, mesma key, toda vez; intenção diferente, key diferente, sempre.
E o passo de registrar a key tem que ser atômico com o efeito. Se você envia a fatura e então, como um passo separado, anota a key, e você crasha na lacuna, você enviou sem lembrar, e o próximo retry envia de novo. O “eu fiz isso” tem que dar commit junto com o fazer, ou a garantia inteira vaza pela rachadura entre eles.
Parte dois: o recibo, verifique o efeito, não o infira
A key impede a mesma tentativa de duplicar. Mas há uma segunda falha, mais silenciosa, que a key sozinha não toca, e é a que o loop ingênuo faz exatamente ao contrário.
Reconsidere a cena original. O agent envia, o trabalho completa do lado de lá, e o acknowledgment é perdido em trânsito. O agent nunca ouviu “feito.” O que ele conclui? Ele conclui que o envio falhou. Com uma idempotency key no lugar, um retry agora é seguro, mas o agent ainda não sabe que a fatura saiu. Ele está adivinhando da ausência de uma confirmação, e ausência não é informação.
Rode isso ao contrário e é igualmente ruim. O agent recebe de volta um animado 200 OK e conclui sucesso, mas a chamada só enfileirou o trabalho, e a fila depois o descartou. O agent ouviu “feito” e a coisa nunca aconteceu. Uma confirmação é uma alegação, e uma alegação não é um resultado.
“Sem exception” não é o mesmo que “aconteceu.” “Uma exception” não é o mesmo que “não aconteceu.”
O loop ingênuo confunde o valor de retorno com a realidade. Ele trata um retorno bem-sucedido como prova de que o efeito ocorreu e um erro lançado como prova de que não, e ambas as inferências estão erradas exatamente nos casos que mordem. O movimento honesto é parar de inferir o efeito a partir de como a chamada retornou, e começar a checar por ele diretamente.
Então depois da ação, o sistema pergunta ao mundo uma pergunta separada. Não “a chamada teve sucesso?” mas “a fatura agora existe?” Ele lê de volta o efeito a partir da fonte da verdade, a fatura está no ledger, a mensagem tem um ID, a cobrança aparece no processador. Se o efeito está lá, a ação está feita, não importa o que a chamada original retornou ou se ela retornou de jeito nenhum. Se o efeito não está lá, a ação não está feita, não importa quão verde a resposta pareceu.
Esta é a parte que as pessoas pulam, porque parece redundante, você acabou de fazer a coisa, por que checar que você a fez? Você checa porque a chamada que faz o trabalho e a realidade do trabalho são dois fatos diferentes, unidos por uma rede que descarta mensagens. O recibo é como você descobre em qual mundo está.
Junte as duas e o loop finalmente diz a verdade
Uma ação confiável é a key e o recibo trabalhando como um par, e você pode ler o fluxo inteiro num fôlego.
Cunhe uma key estável a partir da intenção. Tente a ação carregando aquela key. Se a chamada retorna limpa, não acredite ainda, leia o efeito de volta; o recibo, não a resposta, decide. Se a chamada erra ou simplesmente some, dê retry com segurança, porque a key garante que a segunda tentativa não pode duplicar a primeira. Continue até a leitura de volta confirmar que o efeito é real, ou até você escalar uma falha genuína e verificada para um humano. Em nenhum ponto “nenhuma exception voltou” teve permissão de substituir “o trabalho está feito.”
Note o que cada metade está fazendo. A idempotency key torna seguro tentar de novo, ela remove o custo de estar errado sobre falha. A verificação torna seguro parar, ela remove o custo de estar errado sobre sucesso. Sem a key, todo retry é uma aposta de que a última tentativa de fato falhou. Sem o recibo, todo sucesso é um palpite de que a chamada disse a verdade. Você precisa das duas, porque os dois jeitos que o loop ingênuo mente são imagens espelhadas, e uma guarda pega cada uma.
Um retry não torna uma ação confiável. A key e o recibo tornam, o retry só vira seguro de usar uma vez que eles estão no lugar.
Por que agents tornam isso urgente, não opcional
Engenheiros de sistemas conhecem idempotency keys há anos; processadores de pagamento foram construídos sobre elas. Então por que escrever isso agora? Porque agents mudam o raio de explosão de errar isso.
Uma integração tradicional dá retry num conjunto fixo de operações que um humano conectou antecipadamente. Um engenheiro olhou cada chamada, decidiu se era seguro repeti-la, e construiu a guarda onde era necessária. O conjunto de ações perigosas era pequeno, conhecido, e revisado.
Um agent compõe as ações dele em runtime. Você não entrega a ele três chamadas pré-aprovadas; você entrega tools e goals, e ele decide, turn a turn, o que fazer e quando tentar de novo. O instinto de retry dele é o mesmo loop ingênuo, agora aplicado a operações que nenhum engenheiro pré-classificou. Ele vai alegremente reexecutar um envio porque o último pareceu falhar, e não tem nenhum senso embutido de qual pilha aquele envio pertence. O julgamento que costumava viver na revisão de um engenheiro cuidadoso agora tem que viver no sistema em que o agent roda.
Essa é a linha que sustentamos. Uma tool que um agent pode chamar para fazer algo irreversível no mundo é construída em torno da key e do recibo antes de o agent ter permissão de chamá-la, para que “o agent deu retry” nunca possa virar “o cliente foi cobrado duas vezes.” O agent ganha o direito de ser ansioso. A plataforma por baixo dele é o que torna a ansiedade segura.
A virada: confiabilidade nunca foi a ausência de erros
Tire as keys e os recibos e o que sobra é uma ideia mais silenciosa sobre o que “confiável” sequer significa.
Tendemos a imaginar um sistema confiável como um que não lança erros. Então perseguimos o verde, damos retry até a exception parar, tratamos um retorno limpo como uma consciência limpa, e o chamamos de robusto. Mas a fatura enviada duas vezes não lançou erro. A cobrança que sumiu retornou 200 OK. As falhas mais caras em sistemas reais são as que parecem, por dentro, exatamente sucesso. Um sistema que só sabe como evitar exceptions é cego precisamente às falhas que custam mais.
Confiabilidade de verdade não é a ausência de erros. É a presença de verdade, o sistema sabendo, a cada passo, o que de fato aconteceu no mundo, e recusando confundir um valor de retorno esperançoso com um efeito real. Isso é uma coisa mais difícil de construir que um retry loop. É também a única coisa que te deixa entregar a um agent ansioso uma tool que toca um cliente de verdade e dormir à noite. O retry nunca foi a confiabilidade. A confiabilidade é o momento em que o sistema para de adivinhar e vai olhar.
Esta é uma das coisas chatas e estruturais que construímos na Apollo para que as coisas interessantes sejam seguras, agents que podem agir no mundo real sem transformar um pacote de rede perdido numa cobrança dupla. Se você já entrou em produção com um retry loop e sentiu um pequeno medo frio na primeira vez que ele envolveu um write, você já sabe de qual pilha estamos falando.
A Apollo cuida da operação repetitiva da sua empresa pro seu time não precisar.
Entre na lista de espera: acesso antecipado, preço de usuário fundador e um lugar na primeira fila enquanto a gente constrói.
Entrar na lista de esperaO imposto oculto dos agents em paralelo é um diamante de migrations
Seis agents escrevendo para um schema conflitam no banco de dados, não no código, e a CI morre em "multiple heads".
EngenhariaUm orchestrator que não sobrevive ao próprio crash não é um
Um crash que apaga o raciocínio do orchestrator perde a única coisa que você não consegue reconstruir.
EngenhariaColoque um portão determinístico na frente do seu revisor mais esperto
A pega-defeito mais barata é um script burro que checa se duas branches mergeadas ainda sobem antes de qualquer julgamento.