Menghapus duplikat permintaan tugas

Cloud Scheduler menyertakan identifikasi tugas dan header HTTP waktu jadwal pada permintaan HTTP dan App Engine yang memungkinkan Anda menghapus duplikat eksekusi yang dicoba ulang dan membuat pengendali target Anda idempoten. Karena Cloud Scheduler menyediakan pengiriman setidaknya sekali, penghapusan duplikat permintaan akan mencegah pengendali atau API Google Cloud downstream Anda memproses eksekusi terjadwal yang sama lebih dari sekali saat waktu tunggu habis atau terjadi gangguan jaringan sementara.

Cara kerja penghapusan duplikat permintaan

Permintaan tugas duplikat untuk operasi terjadwal yang sama dapat terjadi dalam situasi berikut:

  • Target yang berjalan lama: Layanan target memerlukan waktu lebih lama untuk merespons daripada batas waktu percobaan (attemptDeadline) tugas, sehingga Cloud Scheduler kehabisan waktu dan mencoba ulang tugas saat percobaan pertama masih berjalan. Untuk target HTTP, attemptDeadline ditetapkan secara default ke 3 menit dan dapat dikonfigurasi hingga 30 menit. Selaraskan attemptDeadline dengan waktu tunggu target Anda, atau proses pekerjaan yang berjalan lama secara asinkron jika eksekusi dapat melebihi 30 menit.
  • Konfirmasi jaringan yang hilang atau percobaan ulang proxy sementara: Target menerima dan memproses permintaan, tetapi gangguan jaringan sementara atau pemutusan koneksi proxy mencegah Cloud Scheduler menerima respons 2xx HTTP. Meskipun Anda menetapkan retryCount ke 0, pengiriman minimal satu kali berarti target masih dapat menerima permintaan duplikat selama kegagalan transportasi sementara.

Agar pengendali tugas Anda idempoten dan menghapus duplikat permintaan yang dicoba ulang, gunakan header HTTP yang otomatis disertakan Cloud Scheduler pada permintaan ke target HTTP dan App Engine:

Header Deskripsi
X-CloudScheduler-JobName ID tugas singkat (JOB_ID) dari projects/PROJECT_ID/locations/LOCATION_ID/jobs/JOB_ID.
X-CloudScheduler-ScheduleTime

Untuk tugas yang ditentukan dalam format unix-cron, waktu eksekusi terjadwal sebagai offset UTC yang diformat sesuai dengan RFC 3339—misalnya, 2026-10-16T12:00:00Z atau 2026-10-16T05:00:00-07:00. Nilai ini tetap konstan di semua upaya percobaan ulang untuk operasi terjadwal yang sama.

Header ini tidak dijamin untuk tugas yang menggunakan format jadwal groc alternatif, seperti every 5 minutes. Jika Anda memicu tugas secara manual (misalnya, dengan menjalankan gcloud scheduler jobs run atau mengklik Paksa tugas berjalan di konsol Google Cloud ), header ini akan menggunakan stempel waktu eksekusi terjadwal berikutnya atau mungkin dihilangkan. Akibatnya, penghapusan duplikat berbasis header akan melewati operasi terjadwal mendatang dan operasi manual tambahan yang dipicu sebelum waktu yang dijadwalkan tersebut.

Mewajibkan autentikasi di endpoint target Anda

Header X-CloudScheduler-* tidak diautentikasi dan dapat ditetapkan oleh pemanggil mana pun. Jangan gunakan header ini untuk autentikasi. Sebagai gantinya, amankan endpoint Anda berdasarkan jenis target:

Menggunakan pencarian header yang tidak peka huruf besar/kecil

Kapitalisasi header dapat bervariasi di berbagai versi HTTP dan infrastruktur (misalnya, X-Cloudscheduler-Jobname di HTTP/1.1 atau huruf kecil x-cloudscheduler-jobname dan x-cloudscheduler-scheduletime di lingkungan HTTP/2 seperti Cloud Run). Selalu gunakan pencarian header yang tidak peka huruf besar/kecil di pengendali Anda.

Menghilangkan ambiguitas ID pekerjaan di seluruh project atau region

Cloud Scheduler tidak otomatis mengirim header project ID atau region. Satu endpoint pengendali mungkin menerima permintaan dari beberapa project atau region yang menggunakan JOB_ID yang sama. Dalam hal ini, teruskan project ID atau region di HttpTarget.headers, AppEngineHttpTarget.headers, atau jalur URL target atau parameter kueri, dan sertakan nilai tersebut dalam kunci penghapusan duplikat Anda.

Menghapus duplikat permintaan di pengendali HTTP

Sebelum menerapkan penghapusan duplikat permintaan di pengendali, selesaikan prasyarat penyiapan berikut:

  • Konfigurasikan jadwal tugas Anda dalam format unix-cron (misalnya, */5 * * * *, bukan every 5 minutes) agar Cloud Scheduler mengirim header X-CloudScheduler-ScheduleTime.
  • Siapkan datastore persisten yang mendukung transaksi atau penulisan kondisional atomik, seperti Firestore, Spanner, atau Memorystore. Konfigurasikan kebijakan time-to-live (TTL) pada koleksi atau tabel (misalnya, pada kolom expire_at) untuk menghapus data penghapusan duplikat lama setelah periode percobaan ulang tugas (maxRetryDuration). TTL Firestore menghapus data yang sudah tidak berlaku secara asinkron, biasanya dalam waktu 24-72 jam. Agar percobaan ulang dapat mengklaim kembali operasi dari instance yang error sebelum pembersihan TTL, simpan juga stempel waktu habis masa berlaku sewa (seperti lease_expires, yang ditetapkan setidaknya ke waktu tunggu permintaan target dan attemptDeadline) dan evaluasi dalam transaksi.

Untuk memproses setiap operasi terjadwal hanya sekali di pengendali HTTP, selesaikan langkah-langkah berikut:

  1. Baca X-CloudScheduler-JobName dan X-CloudScheduler-ScheduleTime dari header permintaan masuk dan hash ke dalam kunci penghapusan duplikat dengan panjang tetap (run_id).
  2. Jalankan transaksi di datastore Anda untuk memeriksa dan mengklaim data untuk kunci penghapusan duplikat:
    • Jika data ada dan status adalah completed, segera tampilkan kode status HTTP 200 OK (atau 204 No Content) tanpa menjalankan logika tugas sehingga Cloud Scheduler berhenti mencoba ulang.
    • Jika data ada, status adalah in_progress, dan sewa (lease_expires) belum berakhir, tampilkan kode status error yang dapat dicoba lagi seperti 409 Conflict agar Cloud Scheduler tetap mengaktifkan kebijakan percobaan ulangnya jika upaya yang sedang berlangsung gagal. Hindari menampilkan 503 Service Unavailable dari target App Engine, karena App Engine memperlakukan 503 sebagai error sistem dan mencoba ulang permintaan tanpa memperhitungkannya terhadap retryCount.
    • Jika data tidak ada atau lease-nya telah berakhir, tulis data dengan status ditetapkan ke in_progress, ID upaya unik (attempt_id), stempel waktu habis masa berlaku lease (lease_expires), dan stempel waktu habis masa berlaku TTL (expire_at).
  3. Setelah mengklaim data, jalankan logika tugas Anda. Dalam transaksi, pastikan bahwa attempt_id data masih cocok dengan upaya saat ini sebelum mengubah data:

    • Jika logika tugas berhasil, perbarui status ke completed dan tampilkan kode status HTTP 200 OK.
    • Jika logika tugas gagal dengan error yang dapat Anda coba lagi, hapus data penghapusan duplikat sebelum menampilkan kode status 5xx agar Cloud Scheduler dapat mencoba lagi operasi tanpa menunggu masa berlaku sewa berakhir.

Contoh pengendali HTTP

Contoh Python berikut (untuk Cloud Run atau Cloud Run Functions) menggunakan transaksi Firestore dengan sewa untuk mengklaim operasi dan menghapus duplikat permintaan tugas yang dicoba ulang.

Contoh ini belum siap produksi. Tinjau dan sesuaikan dengan kebutuhan Anda sebelum men-deploy:

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

Mencegah permintaan duplikat ke target API Google Cloud

Saat Cloud Scheduler memanggil endpoint API Google Cloud (*.googleapis.com) secara langsung melalui HTTP, API target akan mengabaikan header X-CloudScheduler-JobName dan X-CloudScheduler-ScheduleTime. Misalnya, memanggil workflowexecutions.googleapis.com untuk memulai eksekusi Workflows, run.googleapis.com untuk menjalankan tugas Cloud Run, atau cloudbuild.googleapis.com untuk menjalankan pemicu Cloud Build akan memulai operasi baru pada setiap upaya percobaan ulang.

Untuk keandalan produksi, terapkan idempotensi di layanan paling downstream yang mengubah status. Menghapus duplikat permintaan hanya di lapisan upstream akan mempersempit jendela percobaan ulang, tetapi jika permintaan downstream berhasil dan respons HTTP hilang melalui jaringan, pemanggil upstream tidak dapat menentukan apakah operasi telah selesai dan mungkin masih mencoba ulang permintaan.

Untuk mencegah eksekusi duplikat saat menjadwalkan target Google Cloud API, gunakan pendekatan berikut bersama-sama untuk idempotensi menyeluruh:

  • Rutekan permintaan melalui proxy idempoten: Alih-alih memanggil endpoint *.googleapis.com langsung dari Cloud Scheduler, deploy layanan Cloud Run atau proxy Cloud Run Functions Anda sendiri dan konfigurasikan Cloud Scheduler untuk memanggil proxy tersebut. Di proxy, terapkan pola klaim transaksional atomik Python dan Firestore yang dijelaskan dalam dokumen ini, di bagian Menghapus duplikat permintaan di pengendali HTTP.

    Proxy membaca X-CloudScheduler-JobName dan X-CloudScheduler-ScheduleTime, menghapus duplikat permintaan yang dicoba lagi, dan meneruskan kunci penghapusan duplikat sebagai token idempotensi dalam payload permintaan (misalnya, di kolom argument saat memulai eksekusi Workflows).

    Wajibkan autentikasi pada proxy (misalnya, deploy dengan --no-allow-unauthenticated) dan konfigurasikan tugas Cloud Scheduler untuk melakukan autentikasi dengan token OIDC. Jalankan proxy di akun layanan khusus dengan peran Identity and Access Management (IAM) minimum yang diperlukan (seperti roles/workflows.invoker pada alur kerja target dan roles/datastore.user untuk data penghapusan duplikat).

  • Terapkan idempotensi di layanan downstream: Gunakan token idempotensi untuk melakukan pemeriksaan atomik atau transaksional di datastore seperti Firestore sebelum menjalankan langkah-langkah yang mengubah status. Teruskan token ke endpoint HTTP downstream atau fungsi Cloud Run yang dipanggil oleh layanan Anda, sehingga target tersebut juga dapat menghapus duplikat percobaan ulang. Untuk contoh end-to-end yang menggunakan Workflows, transaksi Firestore, dan Cloud Run Functions, lihat postingan blog Google Cloud, Melakukan panggilan API tepat satu kali saat menggunakan Workflows.