Skip to content

Commit b6fe304

Browse files
committed
Handle P2002 race in createDateTimeWaitpoint
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.
1 parent 0f349dd commit b6fe304

1 file changed

Lines changed: 44 additions & 17 deletions

File tree

internal-packages/run-engine/src/engine/systems/waitpointSystem.ts

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -226,25 +226,52 @@ export class WaitpointSystem {
226226
}
227227
}
228228

229-
const waitpoint = await prisma.waitpoint.upsert({
230-
where: {
231-
environmentId_idempotencyKey: {
232-
environmentId,
229+
let waitpoint: Waitpoint;
230+
try {
231+
waitpoint = await prisma.waitpoint.upsert({
232+
where: {
233+
environmentId_idempotencyKey: {
234+
environmentId,
235+
idempotencyKey: idempotencyKey ?? nanoid(24),
236+
},
237+
},
238+
create: {
239+
...WaitpointId.generate(),
240+
type: "DATETIME",
233241
idempotencyKey: idempotencyKey ?? nanoid(24),
242+
idempotencyKeyExpiresAt,
243+
userProvidedIdempotencyKey: !!idempotencyKey,
244+
environmentId,
245+
projectId,
246+
completedAfter,
234247
},
235-
},
236-
create: {
237-
...WaitpointId.generate(),
238-
type: "DATETIME",
239-
idempotencyKey: idempotencyKey ?? nanoid(24),
240-
idempotencyKeyExpiresAt,
241-
userProvidedIdempotencyKey: !!idempotencyKey,
242-
environmentId,
243-
projectId,
244-
completedAfter,
245-
},
246-
update: {},
247-
});
248+
update: {},
249+
});
250+
} catch (error) {
251+
// A concurrent request with the same user-provided idempotencyKey can win the
252+
// race between our findFirst above and this upsert, causing a P2002 on
253+
// @@unique([environmentId, idempotencyKey]). The winner already created the row
254+
// and enqueued the finishWaitpoint job, so return the existing row as cached
255+
// instead of retrying (a plain retry would re-collide on the user-provided key).
256+
if (
257+
idempotencyKey &&
258+
error instanceof Prisma.PrismaClientKnownRequestError &&
259+
error.code === "P2002"
260+
) {
261+
const existing = await prisma.waitpoint.findFirst({
262+
where: {
263+
environmentId,
264+
idempotencyKey,
265+
},
266+
});
267+
268+
if (existing) {
269+
return { waitpoint: existing, isCached: true };
270+
}
271+
}
272+
273+
throw error;
274+
}
248275

249276
await this.$.worker.enqueue({
250277
id: `finishWaitpoint.${waitpoint.id}`,

0 commit comments

Comments
 (0)