Skip to content

Handle P2002 race in createDateTimeWaitpoint#4134

Draft
claude[bot] wants to merge 1 commit into
mainfrom
fix/waitpoint-datetime-p2002-race
Draft

Handle P2002 race in createDateTimeWaitpoint#4134
claude[bot] wants to merge 1 commit into
mainfrom
fix/waitpoint-datetime-p2002-race

Conversation

@claude

@claude claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Changelog

Before: Two genuinely-concurrent requests carrying the same user-provided idempotencyKey for a DATETIME waitpoint could both pass the findFirst existence check, both attempt the INSERT, and the loser would violate @@unique([environmentId, idempotencyKey]). Prisma raised a P2002 (PrismaClientKnownRequestError), which surfaced as a 500.

After: The race is handled idempotently. The request that loses the INSERT race now catches the P2002, fetches the row created by the concurrent winner, and returns it as { waitpoint, isCached: true } — the same shape returned when an existing waitpoint is found up front. No error is surfaced.

How

createDateTimeWaitpoint did a non-atomic check-then-upsert. The upsert is now wrapped in a try/catch. On P2002 — and only when a user-provided idempotencyKey is present (the auto-generated nanoid case effectively never collides) — it re-reads the row by { environmentId, idempotencyKey } and returns it as cached. Because the concurrent winner already created the row and enqueued its finishWaitpoint worker job, the recovery path returns the existing row without re-enqueuing, avoiding a double-scheduled job. A plain retry loop (as used for the auto-key path) is deliberately avoided here since a user-provided key would just re-collide.

The sibling createManualWaitpoint already guards the identical upsert against P2002 (via a retry loop), so this brings the DATETIME path in line. The two functions are intentionally left un-unified to keep this change tight; unifying them is a possible follow-up.

Root cause: non-atomic findFirst-then-upsert in createDateTimeWaitpointinternal-packages/run-engine/src/engine/systems/waitpointSystem.ts:229.


Testing

  • Typecheck: pnpm --filter @internal/run-engine typecheck (i.e. tsc --noEmit -p tsconfig.build.json) — passes cleanly after generating the Prisma client and building workspace dependencies.
  • No regression test added. The existing waitpoint tests in this package (internal-packages/run-engine/src/engine/tests/waitpoints.test.ts, waitpointRace.test.ts) run via containerTest from @internal/testcontainers, which requires Docker-backed Postgres/Redis containers. A meaningful P2002 regression test needs a live DB to actually trigger the unique-constraint violation under real concurrency; that setup is too heavy to add reliably here without introducing flakiness, so it is deferred.

Under concurrency, two requests with the same user-provided idempotencyKey
can both pass the findFirst check and attempt the INSERT, violating the
@@unique([environmentId, idempotencyKey]) constraint and surfacing a P2002
as a 500. Catch P2002 (only when a user-provided key is present), fetch the
row created by the concurrent winner, and return it as cached with idempotent
semantics instead of blindly retrying.
@changeset-bot

changeset-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: b6fe304

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant