Jobanfragen deduplizieren

Cloud Scheduler enthält Jobidentifikations- und Zeitplan-HTTP-Header für HTTP- und App Engine-Anfragen, mit denen Sie wiederholte Ausführungen deduplizieren und Ihre Ziel-Handler idempotent machen können. Da Cloud Scheduler eine At-Least-Once-Zustellung bietet, wird durch die Deduplizierung von Anfragen verhindert, dass Ihr Handler oder nachgelagerte Google Cloud -APIs dieselbe geplante Ausführung mehr als einmal verarbeiten, wenn Zeitüberschreitungen oder vorübergehende Netzwerkunterbrechungen auftreten.

Funktionsweise der Deduplizierung von Anfragen

Doppelte Jobanfragen für dieselbe geplante Ausführung können in den folgenden Situationen auftreten:

  • Ziele mit langer Laufzeit: Der Zieldienst benötigt länger für die Antwort als die Versuchsfrist (attemptDeadline) des Jobs. Dies führt zu einer Zeitüberschreitung in Cloud Scheduler und der Job wird wiederholt, während der erste Versuch noch läuft. Bei HTTP-Zielen beträgt der Standardwert für attemptDeadline 3 Minuten. Er kann auf bis zu 30 Minuten konfiguriert werden. Richten Sie attemptDeadline am Zeitlimit Ihres Ziels aus oder verarbeiten Sie lang andauernde Aufgaben asynchron, wenn die Ausführung länger als 30 Minuten dauern kann.
  • Verlorene Netzwerkbestätigungen oder vorübergehende Proxy-Wiederholungen: Das Ziel empfängt und verarbeitet die Anfrage, aber eine vorübergehende Netzwerkunterbrechung oder Proxy-Trennung verhindert, dass Cloud Scheduler die HTTP-2xx-Antwort empfängt. Auch wenn Sie retryCount auf 0 setzen, bedeutet die „Mindestens einmal“-Zustellung, dass ein Ziel bei vorübergehenden Transportfehlern weiterhin doppelte Anfragen erhalten kann.

Wenn Sie Ihren Job-Handler idempotent machen und wiederholte Anfragen deduplizieren möchten, verwenden Sie die HTTP-Header, die Cloud Scheduler automatisch in Anfragen an HTTP- und App Engine-Ziele einfügt:

Header Beschreibung
X-CloudScheduler-JobName Die kurze Job-ID (JOB_ID) aus projects/PROJECT_ID/locations/LOCATION_ID/jobs/JOB_ID.
X-CloudScheduler-ScheduleTime

Für Jobs im unix-cron-Format die geplante Ausführungszeit als UTC-Offset, formatiert gemäß RFC 3339, z. B. 2026-10-16T12:00:00Z oder 2026-10-16T05:00:00-07:00. Dieser Wert bleibt bei allen Wiederholungsversuchen für dieselbe geplante Ausführung konstant.

Dieser Header ist nicht für Jobs garantiert, die das alternative groc-Zeitplanformat verwenden, z. B. every 5 minutes. Wenn Sie einen Job manuell auslösen (z. B. durch Ausführen von gcloud scheduler jobs run oder Klicken auf Jobausführung erzwingen in der Google Cloud Console), wird in diesem Header der Zeitstempel der nächsten geplanten Ausführung verwendet oder er wird weggelassen. Daher wird die geplante Ausführung und alle zusätzlichen manuellen Ausführungen, die vor der geplanten Zeit ausgelöst werden, bei der headerbasierten Deduplizierung übersprungen.

Authentifizierung am Zielendpunkt anfordern

Die X-CloudScheduler-*-Header sind nicht authentifiziert und können von jedem Aufrufer festgelegt werden. Verwenden Sie diese Header nicht für die Authentifizierung. Sichern Sie stattdessen Ihren Endpunkt basierend auf dem Zieltyp:

Header-Suchvorgänge ohne Berücksichtigung der Groß-/Kleinschreibung verwenden

Die Groß-/Kleinschreibung von Headern kann je nach HTTP-Version und Infrastruktur variieren (z. B. X-Cloudscheduler-Jobname in HTTP/1.1 oder Kleinbuchstaben x-cloudscheduler-jobname und x-cloudscheduler-scheduletime in HTTP/2-Umgebungen wie Cloud Run). Verwenden Sie in Ihrem Handler immer Header-Lookups, bei denen die Groß-/Kleinschreibung nicht berücksichtigt wird.

Job-IDs über Projekte oder Regionen hinweg eindeutig machen

Cloud Scheduler sendet nicht automatisch Projekt-ID- oder Regions-Header. Ein einzelner Handler-Endpunkt kann Anfragen von mehreren Projekten oder Regionen empfangen, die dieselbe JOB_ID verwenden. Übergeben Sie in diesem Fall die Projekt-ID oder Region in HttpTarget.headers, AppEngineHttpTarget.headers oder den Ziel-URL-Pfad oder die Suchparameter und fügen Sie diese Werte in Ihren Deduplizierungsschlüssel ein.

Anfragen in einem HTTP-Handler deduplizieren

Bevor Sie die Deduplizierung von Anfragen in Ihrem Handler implementieren, müssen Sie die folgenden Voraussetzungen erfüllen:

  • Konfigurieren Sie den Zeitplan Ihres Jobs im unix-cron-Format (z. B. */5 * * * * statt every 5 minutes), damit Cloud Scheduler den Header X-CloudScheduler-ScheduleTime sendet.
  • Richten Sie einen nichtflüchtigen Datenspeicher ein, der Transaktionen oder atomare bedingte Schreibvorgänge unterstützt, z. B. Firestore, Spanner oder Memorystore. Konfigurieren Sie eine TTL-Richtlinie (Time-to-Live) für die Sammlung oder Tabelle (z. B. für das Feld expire_at), um alte Deduplizierungsdatensätze nach dem Wiederholungsfenster des Jobs (maxRetryDuration) zu löschen. Die Firestore-TTL löscht abgelaufene Datensätze asynchron, in der Regel innerhalb von 24–72 Stunden. Damit Wiederholungsversuche Ausführungen von abgestürzten Instanzen vor der TTL-Bereinigung zurückfordern können, speichern Sie auch einen Zeitstempel für den Ablauf der Lease (z. B. lease_expires, der mindestens auf das Zeitlimit für Anfragen Ihres Ziels und attemptDeadline festgelegt ist) und werten Sie ihn in einer Transaktion aus.

Führen Sie die folgenden Schritte aus, um jede geplante Ausführung nur einmal in Ihrem HTTP-Handler zu verarbeiten:

  1. X-CloudScheduler-JobName und X-CloudScheduler-ScheduleTime aus den eingehenden Anfrageheadern lesen und in einen Deduplizierungsschlüssel mit fester Länge (run_id) hashen.
  2. Führen Sie eine Transaktion in Ihrem Datenspeicher aus, um den Datensatz für den Deduplizierungsschlüssel zu prüfen und zu beanspruchen:
    • Wenn der Eintrag vorhanden ist und status den Wert completed hat, geben Sie sofort einen HTTP-Statuscode 200 OK (oder 204 No Content) zurück, ohne die Joblogik auszuführen, damit Cloud Scheduler keine weiteren Versuche unternimmt.
    • Wenn der Eintrag vorhanden ist, status den Wert in_progress hat und die Lease (lease_expires) nicht abgelaufen ist, geben Sie einen wiederholbaren Fehlerstatuscode wie 409 Conflict zurück, damit Cloud Scheduler seine Wiederholungsrichtlinie aktiv hält, falls der laufende Versuch fehlschlägt. Vermeiden Sie die Rückgabe von 503 Service Unavailable von App Engine-Zielen, da App Engine 503 als Systemfehler behandelt und die Anfrage wiederholt, ohne sie auf retryCount anzurechnen.
    • Wenn der Eintrag nicht vorhanden ist oder die Freigabe abgelaufen ist, schreiben Sie den Eintrag mit status auf in_progress, einer eindeutigen Versuchs-ID (attempt_id), einem Zeitstempel für den Ablauf der Freigabe (lease_expires) und einem Zeitstempel für den Ablauf der TTL (expire_at).
  3. Führen Sie nach dem Beanspruchen des Datensatzes die Joblogik aus. Prüfen Sie in einer Transaktion, ob der attempt_id des Datensatzes noch mit dem aktuellen Versuch übereinstimmt, bevor Sie den Datensatz ändern:

    • Wenn die Joblogik erfolgreich ist, aktualisieren Sie status auf completed und geben Sie den HTTP-Statuscode 200 OK zurück.
    • Wenn die Joblogik mit einem Fehler fehlschlägt, den Sie wiederholen können, löschen Sie den Deduplizierungseintrag, bevor Sie den Statuscode 5xx zurückgeben, damit Cloud Scheduler die Ausführung wiederholen kann, ohne auf den Ablauf der Lease zu warten.

Beispiel für einen HTTP-Handler

Im folgenden Python-Beispiel (für Cloud Run oder Cloud Run Functions) wird eine Firestore-Transaktion mit einer Lease verwendet, um Ausführungen zu beanspruchen und wiederholte Jobanfragen zu deduplizieren.

Dieses Beispiel ist nicht produktionsreif. Prüfen Sie die Datei und passen Sie sie vor der Bereitstellung an Ihre Anforderungen an:

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

Doppelte Anfragen an Google Cloud API-Ziele verhindern

Wenn Cloud Scheduler einen Google Cloud -API-Endpunkt (*.googleapis.com) direkt über HTTP aufruft, ignoriert die Ziel-API die Header X-CloudScheduler-JobName und X-CloudScheduler-ScheduleTime. Wenn Sie beispielsweise workflowexecutions.googleapis.com aufrufen, um eine Workflow-Ausführung zu starten, run.googleapis.com, um einen Cloud Run-Job auszuführen, oder cloudbuild.googleapis.com, um einen Cloud Build-Trigger auszuführen, wird bei jedem Wiederholungsversuch eine neue Ausführung gestartet.

Erzwingen Sie für die Produktionszuverlässigkeit die Idempotenz im am weitesten nachgelagerten Dienst, der den Status ändert. Wenn Anfragen nur auf einer Upstream-Ebene dedupliziert werden, wird das Wiederholungsfenster verkleinert. Wenn jedoch eine Downstream-Anfrage erfolgreich ist und die HTTP-Antwort über das Netzwerk verloren geht, kann der Upstream-Aufrufer nicht feststellen, ob der Vorgang abgeschlossen wurde, und die Anfrage möglicherweise trotzdem wiederholen.

Wenn Sie doppelte Ausführungen beim Planen von Google Cloud API-Zielen verhindern möchten, verwenden Sie die folgenden Ansätze zusammen, um eine End-to-End-Idempotenz zu erreichen:

  • Anfragen über einen idempotenten Proxy weiterleiten: Anstatt den *.googleapis.com-Endpunkt direkt über Cloud Scheduler aufzurufen, stellen Sie Ihren eigenen Cloud Run-Dienst oder Cloud Run Functions-Proxy bereit und konfigurieren Sie Cloud Scheduler so, dass dieser Proxy aufgerufen wird. Implementieren Sie im Proxy das in diesem Dokument unter Anfragen in einem HTTP-Handler deduplizieren beschriebene atomare Transaktionsanspruchsmuster für Python und Firestore.

    Der Proxy liest X-CloudScheduler-JobName und X-CloudScheduler-ScheduleTime, dedupliziert wiederholte Anfragen und übergibt den Deduplizierungsschlüssel als Idempotenztoken in der Nutzlast der Anfrage (z. B. im Feld argument beim Starten einer Workflows-Ausführung).

    Erfordern Sie eine Authentifizierung auf dem Proxy (z. B. Bereitstellung mit --no-allow-unauthenticated) und konfigurieren Sie den Cloud Scheduler-Job so, dass er sich mit einem OIDC-Token authentifiziert. Führen Sie den Proxy unter einem dedizierten Dienstkonto mit nur den mindestens erforderlichen IAM-Rollen (Identity and Access Management) aus (z. B. roles/workflows.invoker für den Zielworkflow und roles/datastore.user für die Deduplizierungseinträge).

  • Idempotenz in den Downstream-Diensten erzwingen: Verwenden Sie das Idempotenztoken, um eine atomare oder transaktionale Prüfung in einem Datenspeicher wie Firestore durchzuführen, bevor Sie zustandsändernde Schritte ausführen. Übergeben Sie das Token an alle nachgelagerten HTTP-Endpunkte oder Cloud Run-Funktionen, die Ihr Dienst aufruft, damit diese Ziele auch Wiederholungsversuche deduplizieren können. Ein End-to-End-Beispiel mit Workflows, Firestore-Transaktionen und Cloud Run Functions finden Sie im Google CloudBlogpost API-Aufrufe genau einmal ausführen, wenn Workflows verwendet werden.