Um retry pode salvar uma task que falhou. Ele também pode repetir uma escrita que já aconteceu antes de o processo morrer. Se a pipeline grava um arquivo, cria um registro ou cobra uma operação, “tentar de novo” não é uma política de idempotência.

No Cloud Run Jobs, a proteção precisa morar no seu código e no armazenamento durável. Dê uma identidade estável à task, grave progresso fora do container e faça a saída aceitar a mesma chave apenas uma vez. O retry então repete a execução sem transformar uma falha parcial em dois resultados.

Este artigo usa TypeScript ilustrativo e um Job de lote. Para escolher entre Service, Job e worker pool, veja a comparação de recursos do Cloud Run para agentes de longa duração. Aqui o problema é mais estreito: como uma task reiniciada recupera seu trabalho sem duplicar o efeito.

Diagrama mostra uma task do Cloud Run reiniciando e passando por retry, checkpoint, idempotência e verificação.

Regra prática

  • Retry decide quantas vezes uma task pode tentar.
  • Checkpoint decide de onde uma tentativa retoma.
  • Idempotência decide se repetir uma entrada cria outro efeito.
  • Verificação prova que a saída esperada existe uma vez, não apenas que o processo terminou.

Por que o retry não torna a task idempotente?

Em 2026, a documentação de retries e checkpoints do Cloud Run Jobs diz que cada task pode falhar, ser reiniciada e tentar novamente. O padrão documentado é de até 3 retries. Isso melhora a chance de concluir falhas transitórias, mas não impede que duas tentativas alcancem a mesma operação externa.

Imagine uma task que calcula um resultado, grava result.json e cai antes de marcar o estado como concluído. A próxima tentativa não sabe se deve recalcular, substituir o arquivo ou ignorar a etapa. Se a saída for um insert sem chave única, ela pode inserir uma segunda linha. Se for um e-mail ou uma cobrança, o dano não é resolvido diminuindo max-retries.

A primeira separação é simples: retry é uma política de recuperação da plataforma; idempotência é uma propriedade da operação. Você pode configurar zero retries e ainda duplicar o resultado ao executar o Job manualmente. Também pode configurar três retries e evitar duplicação se cada efeito usar a mesma chave e uma gravação condicional.

O que precisa sobreviver ao reinício?

Em 2026, o guia do Google Cloud recomenda tornar Jobs idempotentes e usar checkpoints persistentes para que uma task reiniciada não refaça todo o trabalho (Jobs retries and checkpoints). A memória do processo não é checkpoint: ela desaparece quando a instância é encerrada.

Para cada unidade de trabalho, persista quatro coisas:

  1. Identidade: Job, índice da task e uma chave de negócio que não muda entre tentativas.
  2. Estado: queued, running, checkpointed, done ou failed.
  3. Progresso: o último estágio concluído e a localização do artefato parcial.
  4. Saída: um registro ou objeto endereçado pela mesma chave idempotente.

O guia de execução durável para agentes e pipelines explica a fronteira mais ampla entre checkpoint, fila e workflow. Para um Cloud Run Job, você não precisa começar por um orquestrador. Precisa garantir que uma nova tentativa consiga ler o estado e distinguir “a etapa ainda não começou” de “a saída já foi confirmada”.

Não grave o checkpoint apenas depois de toda a pipeline. Salve cada etapa que produz um artefato útil. Uma reexecução pode então pular a decodificação que já terminou e continuar na etapa de transformação. O checkpoint reduz trabalho repetido; a chave idempotente protege o efeito quando a queda ocorre entre a escrita e a confirmação.

Diagrama mostra uma pipeline com checkpoint persistente e uma chave de idempotência bloqueando uma saída duplicada.

Como escrever uma etapa idempotente em TypeScript?

O Cloud Run fornece o índice da task por CLOUD_RUN_TASK_INDEX, mas a identidade da saída ainda é uma decisão da aplicação. A documentação de criação de Jobs descreve esse índice e o total de tasks. Use-o apenas quando a partição do lote fizer parte do contrato; para uma entidade de negócio, inclua também um ID estável do item.

O exemplo a seguir é ilustrativo. putIfAbsent representa uma operação atômica do banco ou storage. Sem essa condição, duas tentativas concorrentes ainda podem passar pela leitura “não existe” e gravar duas saídas.

type Progress = {
  key: string;
  status: "running" | "checkpointed" | "done";
  artifactKey?: string;
  leaseUntil: number;
};

interface ProgressStore {
  get(key: string): Promise<Progress | null>;
  claim(key: string, leaseUntil: number): Promise<"claimed" | "busy" | "done">;
  put(value: Progress): Promise<void>;
  putIfAbsent(key: string, value: { artifactKey: string }): Promise<boolean>;
}

function outputKey(jobName: string, taskIndex: string, itemId: string) {
  return `jobs/${jobName}/tasks/${taskIndex}/items/${itemId}/result.json`;
}

async function runItem(input: {
  jobName: string;
  taskIndex: string;
  itemId: string;
  store: ProgressStore;
  buildArtifact: () => Promise<string>;
}) {
  const key = outputKey(input.jobName, input.taskIndex, input.itemId);
  const existing = await input.store.get(key);

  if (existing?.status === "done") return existing.artifactKey;
  const claim = await input.store.claim(key, Date.now() + 5 * 60_000);
  if (claim === "done") return existing?.artifactKey;
  if (claim === "busy") {
    throw new Error("item is already leased");
  }

  const artifactKey = existing?.artifactKey ?? await input.buildArtifact();
  await input.store.put({ key, status: "checkpointed", artifactKey, leaseUntil: 0 });
  await input.store.putIfAbsent(key, { artifactKey });
  await input.store.put({ key, status: "done", artifactKey, leaseUntil: 0 });

  return artifactKey;
}

O contrato tem duas decisões que não podem ficar implícitas. O claim precisa ser atômico, e o lease precisa expirar porque um processo pode morrer sem liberar running. A escrita final precisa ser condicional ou usar uma chave única. Em produção, também faça a transição de estado com uma operação atômica. O trecho não escolhe Firestore, Cloud Storage ou PostgreSQL; ele mostra as garantias que qualquer adaptador deve oferecer.

Para efeitos externos, como uma chamada de ferramenta que cria um recurso, envie a mesma chave de idempotência ao sistema de destino quando ele suportar isso. Se não suportar, coloque a operação atrás de uma tabela de intenções ou de uma aprovação. A validação de tool calls em TypeScript ajuda a controlar o schema, mas não substitui a proteção contra repetição do efeito.

Como configurar retries e timeout no Cloud Run Job?

Em 2026, a documentação de criação de Jobs informa que o padrão de timeout de uma task é 10 minutos e que o número de retries pode ser configurado. A documentação de timeout de tasks permite aumentar o limite para até 168 horas em tasks sem GPU. Esses valores se aplicam a cada tentativa, não a uma garantia de duração total sem falhas.

Um comando inicial pode ser:

gcloud run jobs create process-items \
  --image=REGION-docker.pkg.dev/PROJECT/REPOSITORY/IMAGE:TAG \
  --tasks=1 \
  --max-retries=3 \
  --task-timeout=20m \
  --region=REGION

O comando é um exemplo de configuração, não um deploy pronto. Confirme a região, a imagem, a conta de serviço e os limites do seu projeto. Comece com retries suficientes para falhas transitórias, mas não trate o número como correção para um efeito não idempotente.

Se o lote puder ser dividido em tasks independentes, use a identidade da task para criar partições repetíveis. O código precisa mapear o mesmo item para a mesma partição em toda tentativa. Se a lista de entrada mudar durante a execução, grave a versão ou o snapshot usado. Sem isso, a mesma task pode processar itens diferentes no retry e dificultar a auditoria.

Como separar falha retryable de erro terminal?

O Google Cloud documenta retries para falhas que podem ser transitórias, mas a aplicação ainda precisa decidir quando uma nova tentativa ajuda. Uma resposta 429, timeout de rede ou erro 5xx pode merecer retry. Um arquivo ausente, schema inválido ou permissão negada normalmente precisa de correção ou quarentena.

Use uma classificação explícita:

  1. Registre o item, a tentativa, o estágio e o motivo antes de lançar o erro.
  2. Lance apenas falhas que podem mudar sem alteração do input ou da configuração.
  3. Marque erros terminais como failed com contexto suficiente para reparo.
  4. Envie a saída de retries esgotados para uma fila ou relatório de dead letter.
  5. Faça um watchdog procurar leases vencidos e estados que ficaram em running.

Não confunda “a função retornou erro” com “o efeito não aconteceu”. O processo pode ter escrito no banco e cair antes do log final. Por isso a recuperação deve consultar o estado durável antes de executar novamente, e não apenas procurar a última linha do log.

Como provar que uma reexecução não duplicou a saída?

Depois de qualquer configuração, teste uma queda em pontos diferentes da etapa: antes do checkpoint, depois do artefato e antes da confirmação final. A execução do Job só deve ser considerada segura quando a segunda tentativa encontra a mesma identidade e produz o mesmo resultado observável.

Uma verificação mínima pode ser descrita assim:

it("reutiliza a saída quando a mesma task é executada duas vezes", async () => {
  const store = makeInMemoryStore();
  let builds = 0;

  const input = {
    jobName: "process-items",
    taskIndex: "0",
    itemId: "item-7",
    store,
    buildArtifact: async () => {
      builds += 1;
      return "artifacts/item-7.json";
    },
  };

  await runItem(input);
  await runItem(input);

  expect(builds).toBe(1);
  expect(await store.countOutputs("item-7")).toBe(1);
});

O teste acima é uma especificação de comportamento; makeInMemoryStore e countOutputs precisam existir na suíte real. Complete-o com um teste que simule uma queda depois de buildArtifact e outro que simule dois workers tentando o mesmo lease. O resultado importante não é uma string idêntica. É a cardinalidade da saída, a chave persistida e o estado final.

Também verifique a plataforma. A documentação de Jobs informa que, quando uma task excede o máximo de retries, a execução do Job termina como falha depois de tentar as tasks. Consulte a execução e os logs, confirme qual task falhou e compare o número de artefatos com o snapshot de entrada. Um Job verde sem essa checagem ainda pode ter processado uma entrada errada.

Erros comuns e limites

O primeiro erro é usar --max-retries=0 como substituto para idempotência. Isso apenas desliga o retry automático. Uma execução manual, uma mudança de input ou uma chamada repetida pode continuar criando efeitos duplicados.

O segundo é salvar somente o arquivo final. Se uma etapa cara termina e a etapa seguinte falha, a reexecução não sabe se o arquivo é válido, de qual versão veio ou qual input o produziu. Inclua versão do input, nome da etapa e timestamp no registro de progresso.

O terceiro é tornar o estado durável, mas não a escrita do efeito. Um registro done gravado antes da chamada externa pode esconder uma falha. Grave a intenção, execute com chave idempotente quando possível e só confirme o estado depois de verificar o resultado.

O quarto é colocar tudo em uma única task longa. O teste de agentes com mock e API real ajuda a verificar se a execução repetida chama a dependência externa na hora certa. Para uma pipeline com várias etapas, checkpoints endereçáveis permitem repetir só a parte necessária. Mesmo assim, não trate checkpoints como transação distribuída: uma queda pode acontecer entre dois sistemas, e a recuperação precisa ser segura nesse intervalo.

Perguntas frequentes sobre retries no Cloud Run Jobs

Quantos retries um Cloud Run Job faz por padrão?

A documentação de criação de Jobs informa que o padrão é de até 3 retries por task, e o valor pode ser configurado entre 0 e 10. O retry é por task, não uma garantia de que o efeito externo será aplicado uma única vez. Use o valor para falhas transitórias e implemente idempotência separadamente.

Um checkpoint elimina a necessidade de idempotência?

Não. O checkpoint reduz o trabalho repetido e indica de onde retomar, mas a queda pode acontecer depois da escrita e antes do checkpoint. A documentação de retries recomenda as duas práticas. Use esses dois controles: progresso persistente para recuperar a etapa e uma chave única ou escrita condicional para proteger a saída.

Qual é o timeout padrão de uma task?

Em 2026, o timeout padrão documentado para uma task é de 10 minutos. Ele pode ser reduzido ou aumentado até 168 horas para tasks sem GPU, enquanto tasks com GPU têm limite de 1 hora. O timeout vale para cada tentativa. Divida o trabalho ou salve checkpoints quando uma etapa puder ultrapassar esse limite.

max-retries=0 impede toda duplicação?

Não. Com 0 retries automáticos, ele impede novas tentativas depois de uma falha, mas não impede execuções manuais, reentregas do mesmo input ou dois workers disputando a mesma chave. A defesa é uma identidade determinística, lease com expiração, escrita condicional e uma verificação que conte a saída confirmada.

Fontes consultadas