Eliminar a duplicação de solicitações de job

O Cloud Scheduler inclui identificação de jobs e cabeçalhos HTTP de horário de programação em solicitações HTTP e do App Engine que permitem eliminar duplicação de execuções repetidas e tornar os gerenciadores de destino idempotentes. Como o Cloud Scheduler oferece entrega pelo menos uma vez, a eliminação de duplicação de solicitações impede que o gerenciador ou as APIs Google Cloud downstream processem a mesma execução programada mais de uma vez quando ocorrem tempos limite ou interrupções de rede temporárias.

Como funciona a eliminação de duplicação de solicitações

Solicitações de job duplicadas para a mesma execução programada podem ocorrer nas seguintes situações:

  • Destinos de longa duração: o serviço de destino leva mais tempo para responder do que o prazo de tentativa do job (attemptDeadline), fazendo com que o Cloud Scheduler expire e tente novamente o job enquanto a primeira tentativa ainda está em execução. Para destinos HTTP, attemptDeadline assume como padrão 3 minutos e pode ser configurado para até 30 minutos. Alinhe attemptDeadline com o tempo limite do destino ou processe o trabalho de longa duração de forma assíncrona se a execução puder exceder 30 minutos.
  • Confirmações de rede perdidas ou novas tentativas de proxy temporárias: o destino recebe e processa a solicitação, mas uma interrupção temporária da rede ou uma desconexão do proxy impede que o Cloud Scheduler receba a resposta HTTP 2xx. Mesmo que você defina retryCount como 0, a entrega pelo menos uma vez significa que um destino ainda pode receber solicitações duplicadas durante falhas de transporte temporárias.

Para tornar o gerenciador de jobs idempotente e eliminar a duplicação de solicitações repetidas, use os cabeçalhos HTTP que o Cloud Scheduler inclui automaticamente nas solicitações para destinos HTTP e do App Engine:

Cabeçalho Descrição
X-CloudScheduler-JobName O identificador de job curto (JOB_ID) de projects/PROJECT_ID/locations/LOCATION_ID/jobs/JOB_ID.
X-CloudScheduler-ScheduleTime

Para jobs especificados no formato unix-cron, o horário de execução programado como um deslocamento de UTC formatado de acordo com a RFC 3339, por exemplo, 2026-10-16T12:00:00Z ou 2026-10-16T05:00:00-07:00. Esse valor permanece constante em todas as tentativas de repetição da mesma execução programada.

Esse cabeçalho não é garantido para jobs que usam o formato de programação alternativo do groc, como every 5 minutes. Se você acionar um job manualmente (por exemplo, executando gcloud scheduler jobs run ou clicando em Forçar a execução de um job no console do Google Cloud ), esse cabeçalho usará o carimbo de data/hora da próxima execução programada ou poderá ser omitido. Como resultado, a eliminação de duplicação baseada em cabeçalho ignora a próxima execução programada e todas as execuções manuais adicionais acionadas antes do horário programado.

Exigir autenticação no endpoint de destino

Os cabeçalhos X-CloudScheduler-* não são autenticados e podem ser definidos por qualquer autor da chamada. Não use esses cabeçalhos para autenticação. Em vez disso, proteja o endpoint com base no tipo de destino:

Usar pesquisas de cabeçalho que não diferenciam maiúsculas de minúsculas

As letras maiúsculas e minúsculas do cabeçalho podem variar de acordo com as versões e a infraestrutura do HTTP (por exemplo, X-Cloudscheduler-Jobname em HTTP/1.1 ou x-cloudscheduler-jobname e x-cloudscheduler-scheduletime em minúsculas em ambientes HTTP/2, como o Cloud Run). Sempre use pesquisas de cabeçalho que não diferenciam maiúsculas de minúsculas no gerenciador.

Desambiguar códigos da tarefa em projetos ou regiões

O Cloud Scheduler não envia automaticamente cabeçalhos de ID do projeto ou de região. Um único endpoint de gerenciador pode receber solicitações de vários projetos ou regiões que compartilham o mesmo JOB_ID. Nesse caso, transmita o ID do projeto ou a região em HttpTarget.headers, AppEngineHttpTarget.headers, ou o caminho do URL de destino ou os parâmetros de consulta e inclua esses valores na sua chave de eliminação de duplicação.

Eliminar a duplicação de solicitações em um gerenciador HTTP

Antes de implementar a eliminação de duplicação de solicitações no gerenciador, conclua os seguintes pré-requisitos de configuração:

  • Configure a programação do job no formato unix-cron (por exemplo, */5 * * * * em vez de every 5 minutes) para que o Cloud Scheduler envie o cabeçalho X-CloudScheduler-ScheduleTime.
  • Configure um armazenamento de dados persistente que ofereça suporte a transações ou gravações condicionais atômicas, como Firestore, Spanner ou Memorystore. Configure uma política de time to live (TTL) na coleção ou tabela (por exemplo, no campo expire_at) para excluir registros antigos de eliminação de duplicação após a janela de repetição do job (maxRetryDuration). O TTL do Firestore exclui registros expirados de forma assíncrona, normalmente em 24 a 72 horas. Para permitir que as novas tentativas recuperem execuções de instâncias com falha antes da limpeza do TTL, armazene também um carimbo de data/hora de expiração da concessão (como lease_expires, definido pelo menos como o tempo limite de solicitação do destino e attemptDeadline) e avalie-o em uma transação.

Para processar cada execução programada apenas uma vez no gerenciador HTTP, siga estas etapas:

  1. Leia X-CloudScheduler-JobName e X-CloudScheduler-ScheduleTime dos cabeçalhos da solicitação recebidos e faça o hash deles em uma chave de eliminação de duplicação de tamanho fixo (run_id).
  2. Execute uma transação no seu repositório de dados para verificar e reivindicar o registro da chave de eliminação de duplicação:
    • Se o registro existir e status for completed, retorne um código de status HTTP 200 OK (ou 204 No Content) imediatamente sem executar a lógica do job para que o Cloud Scheduler pare de tentar novamente.
    • Se o registro existir, status for in_progress e a concessão (lease_expires) não tiver expirado, retorne um código de status de erro que permita novas tentativas, como 409 Conflict, para que o Cloud Scheduler mantenha a política de novas tentativas ativa caso a tentativa em andamento falhe. Evite retornar 503 Service Unavailable de destinos do App Engine, porque o App Engine trata 503 como um erro do sistema e repete a solicitação sem contabilizá-la em retryCount.
    • Se o registro não existir ou a concessão dele tiver expirado, grave o registro com status definido como in_progress, um ID de tentativa exclusivo (attempt_id), um carimbo de data/hora de expiração da concessão (lease_expires) e um carimbo de data/hora de expiração do TTL (expire_at).
  3. Depois de reivindicar o registro, execute a lógica do job. Em uma transação, verifique se o attempt_id do registro ainda corresponde à tentativa atual antes de modificar o registro:

    • Se a lógica do job for bem-sucedida, atualize status para completed e retorne um código de status HTTP 200 OK.
    • Se a lógica do job falhar com um erro que pode ser repetido, exclua o registro de eliminação de duplicação antes de retornar um código de status 5xx para que o Cloud Scheduler possa repetir a execução sem esperar que a concessão expire.

Exemplo de gerenciador HTTP

O exemplo em Python a seguir (para o Cloud Run ou Cloud Run functions) usa uma transação do Firestore com uma concessão para reivindicar execuções e eliminar a duplicação de solicitações de job repetidas.

Este exemplo não está pronto para produção. Revise e ajuste-o às suas necessidades antes da implantação:

import datetime
import hashlib
import uuid

import flask
import functions_framework
from google.cloud import firestore

# Longer than the work takes, and at least the job's attempt deadline.
LEASE = datetime.timedelta(minutes=5)
# Keep records past the job's retry window. A TTL policy on expire_at deletes them.
RETENTION = datetime.timedelta(days=1)

db = firestore.Client()


def run_job_logic() -> None:
    """Does the scheduled work. Replace the body with your own task."""


@firestore.transactional
def claim_run(
    transaction: firestore.Transaction,
    run_ref: firestore.DocumentReference,
    attempt_id: str,
    now: datetime.datetime,
) -> str:
    """Claims the run unless it's done or another attempt holds the lease."""
    snapshot = run_ref.get(transaction=transaction)
    if snapshot.exists:
        run = snapshot.to_dict()
        if run["status"] == "completed":
            return "completed"
        if run["lease_expires"] > now:
            return "in_progress"
    transaction.set(
        run_ref,
        {
            "status": "in_progress",
            "attempt_id": attempt_id,
            "lease_expires": now + LEASE,
            "expire_at": now + RETENTION,
        },
    )
    return "claimed"


@firestore.transactional
def finish_run(
    transaction: firestore.Transaction,
    run_ref: firestore.DocumentReference,
    attempt_id: str,
    succeeded: bool,
) -> None:
    """Completes or releases the run, if this attempt still holds the claim."""
    snapshot = run_ref.get(transaction=transaction)
    if not snapshot.exists or snapshot.to_dict().get("attempt_id") != attempt_id:
        return  # A retry claimed the run after this attempt's lease expired.
    if succeeded:
        transaction.update(run_ref, {"status": "completed"})
    else:
        transaction.delete(run_ref)


@functions_framework.http
def handle_scheduled_job(request: flask.Request) -> tuple[str, int]:
    """Runs a job's work once per scheduled run, even if the run is delivered twice."""
    job_name = request.headers.get("X-CloudScheduler-JobName")
    schedule_time = request.headers.get("X-CloudScheduler-ScheduleTime")
    if not job_name or not schedule_time:
        return "error: missing Cloud Scheduler headers", 400

    # Retries keep the same schedule time. A forced run uses the next
    # scheduled time, so that scheduled run is then skipped.
    run_id = hashlib.sha256(f"{job_name}/{schedule_time}".encode()).hexdigest()
    run_ref = db.collection("scheduler_runs").document(run_id)
    now = datetime.datetime.now(datetime.timezone.utc)
    attempt_id = uuid.uuid4().hex

    status = claim_run(db.transaction(), run_ref, attempt_id, now)
    if status == "completed":
        return "Duplicate run skipped", 200
    if status == "in_progress":
        # A non-2xx response keeps retries going in case that attempt fails.
        return "Run in progress", 409

    try:
        run_job_logic()
    except Exception:
        # Releases the claim so that a retry can process the run.
        finish_run(db.transaction(), run_ref, attempt_id, succeeded=False)
        raise
    finish_run(db.transaction(), run_ref, attempt_id, succeeded=True)
    return "Run processed", 200

Evitar solicitações duplicadas para destinos de API do Google Cloud

Quando o Cloud Scheduler invoca um endpoint de API do Google Cloud (*.googleapis.com) diretamente por HTTP, a API de destino ignora os cabeçalhos X-CloudScheduler-JobName e X-CloudScheduler-ScheduleTime. Por exemplo, chamar workflowexecutions.googleapis.com para iniciar uma execução do Workflows, run.googleapis.com para executar um job do Cloud Run ou cloudbuild.googleapis.com para executar um gatilho de build do Cloud Build inicia uma nova execução em cada tentativa.

Para confiabilidade da produção, aplique a idempotência no serviço mais downstream que modifica o estado. Eliminar a duplicação de solicitações apenas em uma camada upstream reduz a janela de repetição, mas se uma solicitação downstream for bem-sucedida e a resposta HTTP for perdida na rede, o autor da chamada upstream não poderá determinar se a operação foi concluída e ainda poderá repetir a solicitação.

Para evitar execuções duplicadas ao programar destinos da API Google Cloud , use as seguintes abordagens juntas para garantir a idempotência de ponta a ponta:

  • Encaminhar solicitações por um proxy idempotente: em vez de chamar o endpoint *.googleapis.com diretamente do Cloud Scheduler, implante seu próprio serviço do Cloud Run ou proxy de função do Cloud Run e configure o Cloud Scheduler para invocar esse proxy. No proxy, implemente o padrão de declaração transacional atômica do Python e do Firestore descrito neste documento, em Eliminar duplicação de solicitações em um gerenciador HTTP.

    O proxy lê X-CloudScheduler-JobName e X-CloudScheduler-ScheduleTime, elimina a duplicação de solicitações repetidas e transmite a chave de eliminação de duplicação como um token de idempotência no payload da solicitação (por exemplo, no campo argument ao iniciar uma execução do Workflows).

    Exija autenticação no proxy (por exemplo, implante com --no-allow-unauthenticated) e configure o job do Cloud Scheduler para autenticar com um token OIDC. Execute o proxy em uma conta de serviço dedicada com apenas os papéis mínimos necessários do Identity and Access Management (IAM), como roles/workflows.invoker no fluxo de trabalho de destino e roles/datastore.user para os registros de eliminação de duplicação.

  • Aplicar idempotência nos serviços downstream: use o token de idempotência para realizar uma verificação atômica ou transacional em um repositório de dados, como o Firestore, antes de executar as etapas de mudança de estado. Transmita o token para qualquer endpoint HTTP downstream ou Cloud Run functions que seu serviço invoque para que esses destinos também possam eliminar a duplicação de novas tentativas. Para um exemplo completo usando o Workflows, as transações do Firestore e o Cloud Run functions, consulte a Google Cloud postagem do blog Como fazer chamadas de API exatamente uma vez ao usar o Workflows.