Anula la duplicación de solicitudes de trabajos

Cloud Scheduler incluye encabezados HTTP de identificación de trabajos y de hora de programación en las solicitudes HTTP y de App Engine que te permiten anular el duplicado de las ejecuciones reintentadas y hacer que tus controladores de destino sean idempotentes. Debido a que Cloud Scheduler proporciona una entrega al menos una vez, la anulación de duplicación de solicitudes evita que tu controlador o las APIs de Google Cloud descendentes procesen la misma ejecución programada más de una vez cuando se producen tiempos de espera o interrupciones de red temporales.

Cómo funciona la anulación de duplicación de solicitudes

Las solicitudes de trabajos duplicadas para la misma ejecución programada pueden ocurrir en las siguientes situaciones:

  • Destinos de larga duración: El servicio de destino tarda más en responder que la fecha límite de intento del trabajo (attemptDeadline), lo que hace que Cloud Scheduler agote el tiempo de espera y vuelva a intentar el trabajo mientras el primer intento aún se está ejecutando. Para los destinos HTTP, attemptDeadline se establece de forma predeterminada en 3 minutos y se puede configurar hasta 30 minutos. Alinea attemptDeadline con el tiempo de espera de tu destino o procesa el trabajo de larga duración de forma asíncrona si la ejecución puede superar los 30 minutos.
  • Confirmaciones de red perdidas o reintentos de proxy transitorios: El destino recibe y procesa la solicitud, pero una interrupción de red transitoria o una desconexión del proxy impiden que Cloud Scheduler reciba la respuesta HTTP 2xx. Incluso si configuras retryCount como 0, la entrega de al menos una vez significa que un destino aún puede recibir solicitudes duplicadas durante fallas de transporte temporales.

Para que el controlador de trabajos sea idempotente y se anule la duplicación de las solicitudes reintentadas, usa los encabezados HTTP que Cloud Scheduler incluye automáticamente en las solicitudes a los destinos HTTP y de App Engine:

Encabezado Descripción
X-CloudScheduler-JobName Es el identificador corto del trabajo (JOB_ID) de projects/PROJECT_ID/locations/LOCATION_ID/jobs/JOB_ID.
X-CloudScheduler-ScheduleTime

Para los trabajos especificados en formato unix-cron, la hora de ejecución programada como un desplazamiento de UTC con formato según RFC 3339, por ejemplo, 2026-10-16T12:00:00Z o 2026-10-16T05:00:00-07:00. Este valor permanece constante en todos los reintentos de la misma ejecución programada.

No se garantiza este encabezado para los trabajos que usan el formato de programa alternativo de groc, como every 5 minutes. Si activas un trabajo de forma manual (por ejemplo, ejecutando gcloud scheduler jobs run o haciendo clic en Forzar la ejecución de un trabajo en la consola de Google Cloud ), este encabezado usa la marca de tiempo de la próxima ejecución programada o se puede omitir. Como resultado, la anulación de duplicación basada en encabezados omite esa próxima ejecución programada y cualquier ejecución manual adicional que se active antes de esa hora programada.

Exige autenticación en tu extremo de destino

Los encabezados X-CloudScheduler-* no se autentican y cualquier emisor puede configurarlos. No uses estos encabezados para la autenticación. En su lugar, protege tu extremo según el tipo de destino:

Usa búsquedas de encabezados que no distingan mayúsculas de minúsculas

El uso de mayúsculas en los encabezados puede variar según las versiones de HTTP y la infraestructura (por ejemplo, X-Cloudscheduler-Jobname en HTTP/1.1 o minúsculas x-cloudscheduler-jobname y x-cloudscheduler-scheduletime en entornos HTTP/2, como Cloud Run). Usa siempre búsquedas de encabezados que no distingan mayúsculas de minúsculas en tu controlador.

Elimina la ambigüedad de los IDs de trabajos en proyectos o regiones

Cloud Scheduler no envía automáticamente encabezados de ID del proyecto ni de región. Un solo extremo de controlador puede recibir solicitudes de varios proyectos o regiones que comparten el mismo JOB_ID. En este caso, pasa el ID del proyecto o la región en HttpTarget.headers, AppEngineHttpTarget.headers, o la ruta de URL de destino o los parámetros de consulta, y, luego, incluye esos valores en tu clave de anulación de duplicación.

Anula la duplicación de solicitudes en un controlador HTTP

Antes de implementar la anulación de duplicación de solicitudes en tu controlador, completa los siguientes requisitos previos de configuración:

  • Configura el programa de tu trabajo en el formato unix-cron (por ejemplo, */5 * * * * en lugar de every 5 minutes) para que Cloud Scheduler envíe el encabezado X-CloudScheduler-ScheduleTime.
  • Configura un almacén de datos persistente que admita transacciones o escrituras condicionales atómicas, como Firestore, Spanner o Memorystore. Configura una política de tiempo de actividad (TTL) en la colección o tabla (por ejemplo, en el campo expire_at) para borrar los registros de anulación de duplicación antiguos después de la ventana de reintento del trabajo (maxRetryDuration). El TTL de Firestore borra los registros vencidos de forma asíncrona, por lo general, en un plazo de 24 a 72 horas. Para permitir que los reintentos reclamen ejecuciones de instancias con fallas antes de la limpieza del TTL, almacena también una marca de tiempo de vencimiento de la asignación (como lease_expires, establecida en, al menos, el tiempo de espera de la solicitud del destino y attemptDeadline) y evalúala en una transacción.

Para procesar cada ejecución programada solo una vez en tu controlador HTTP, completa los siguientes pasos:

  1. Lee X-CloudScheduler-JobName y X-CloudScheduler-ScheduleTime de los encabezados de la solicitud entrante y genera un hash para convertirlos en una clave de anulación de duplicación de longitud fija (run_id).
  2. Ejecuta una transacción en tu almacén de datos para verificar y reclamar el registro de la clave de anulación de duplicación:
    • Si el registro existe y status es completed, devuelve un código de estado HTTP 200 OK (o 204 No Content) de inmediato sin ejecutar la lógica del trabajo para que Cloud Scheduler deje de reintentar.
    • Si el registro existe, status es in_progress y el arrendamiento (lease_expires) no venció, devuelve un código de estado de error que se pueda reintentar, como 409 Conflict, para que Cloud Scheduler mantenga activa su política de reintentos en caso de que falle el intento en curso. Evita devolver 503 Service Unavailable de los destinos de App Engine, ya que App Engine trata 503 como un error del sistema y vuelve a intentar la solicitud sin contarla en retryCount.
    • Si el registro no existe o su arrendamiento venció, escribe el registro con status establecido en in_progress, un ID de intento único (attempt_id), una marca de tiempo de vencimiento del arrendamiento (lease_expires) y una marca de tiempo de vencimiento del TTL (expire_at).
  3. Después de reclamar el registro, ejecuta la lógica del trabajo. En una transacción, verifica que el attempt_id del registro aún coincida con el intento actual antes de modificar el registro:

    • Si la lógica del trabajo se ejecuta de forma correcta, actualiza status a completed y devuelve un código de estado HTTP 200 OK.
    • Si la lógica del trabajo falla con un error que puedes reintentar, borra el registro de anulación de duplicación antes de devolver un código de estado 5xx para que Cloud Scheduler pueda reintentar la ejecución sin esperar a que venza el arrendamiento.

Ejemplo de controlador HTTP

En el siguiente ejemplo de Python (para Cloud Run o Cloud Run Functions), se usa una transacción de Firestore con un arrendamiento para reclamar ejecuciones y anular la duplicación de solicitudes de trabajos reintentados.

Esta muestra no está lista para producción. Revísalo y ajústalo a tus necesidades antes de implementarlo:

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

Evita solicitudes duplicadas a destinos de la API de Google Cloud

Cuando Cloud Scheduler invoca un extremo de la API de Google Cloud (*.googleapis.com) directamente a través de HTTP, la API de destino ignora los encabezados X-CloudScheduler-JobName y X-CloudScheduler-ScheduleTime. Por ejemplo, llamar a workflowexecutions.googleapis.com para iniciar una ejecución de Workflows, run.googleapis.com para ejecutar un trabajo de Cloud Run o cloudbuild.googleapis.com para ejecutar un activador de compilación de Cloud Build inicia una ejecución nueva en cada intento de reintento.

Para garantizar la confiabilidad de la producción, aplica la idempotencia en el servicio más descendente que modifica el estado. La anulación de duplicación de solicitudes solo en una capa upstream reduce el período de reintentos, pero si una solicitud downstream se realiza correctamente y la respuesta HTTP se pierde en la red, el emisor upstream no puede determinar si la operación se completó y es posible que vuelva a intentar la solicitud.

Para evitar ejecuciones duplicadas cuando programes destinos de la API de Google Cloud , usa los siguientes enfoques en conjunto para lograr la idempotencia de extremo a extremo:

  • Enruta las solicitudes a través de un proxy idempotente: En lugar de llamar al extremo de *.googleapis.com directamente desde Cloud Scheduler, implementa tu propio servicio de Cloud Run o proxy de Cloud Run Functions y configura Cloud Scheduler para que invoque ese proxy. En el proxy, implementa el patrón de reclamación transaccional atómico de Python y Firestore que se describe en este documento, en Anula el duplicado de solicitudes en un controlador HTTP.

    El proxy lee X-CloudScheduler-JobName y X-CloudScheduler-ScheduleTime, quita los duplicados de las solicitudes reintentadas y pasa la clave de anulación de duplicación como un token de idempotencia en la carga útil de la solicitud (por ejemplo, en el campo argument cuando se inicia una ejecución de Workflows).

    Exige la autenticación en el proxy (por ejemplo, implementa con --no-allow-unauthenticated) y configura el trabajo de Cloud Scheduler para autenticar con un token de OIDC. Ejecuta el proxy en una cuenta de servicio dedicada con solo los roles de Identity and Access Management (IAM) mínimos requeridos (como roles/workflows.invoker en el flujo de trabajo de destino y roles/datastore.user para los registros de anulación de duplicación).

  • Aplica la idempotencia en los servicios downstream: Usa el token de idempotencia para realizar una verificación atómica o transaccional en un almacén de datos, como Firestore, antes de ejecutar los pasos que cambian el estado. Pasa el token a cualquier extremo HTTP o función de Cloud Run downstream que invoque tu servicio para que esos destinos también puedan anular la duplicación de los reintentos. Para ver un ejemplo de extremo a extremo con Workflows, transacciones de Firestore y Cloud Run Functions, consulta la entrada de blog de Google Cloud, Realiza llamadas a la API exactamente una vez cuando usas Workflows.