Skip to content

Commit f3f6096

Browse files
committed
refactor(run-engine): move the BATCH waitpoint create onto the coordinator seam
blockRunWithCreatedBatch built its waitpoint with runStore.createWaitpoint directly, so it had no arm to route to. It now goes through the coordinator. The P2002 catch moves to the legacy arm, where it belongs: it is the duplicate-batch contract for a unique index, and it is dead against a store that reports a duplicate through NX instead. Leaving it wrapped around a store create would read a genuine store error as a duplicate batch. The block step keeps its own P2002 catch. The previous shape wrapped the create and the block in one try, so a P2002 from either returned null; narrowing that here would be a behaviour change smuggled into an extraction. Seam also gains the mint kind on the create params and batchWaitpointId on the lockless params. Both are pinned to their legacy values at every call site, so behaviour is unchanged.
1 parent f223c68 commit f3f6096

4 files changed

Lines changed: 105 additions & 26 deletions

File tree

internal-packages/run-engine/src/engine/index.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
generateInternalId,
2727
parseNaturalLanguageDurationInMs,
2828
RunId,
29-
WaitpointId,
3029
} from "@trigger.dev/core/v3/isomorphic";
3130
import {
3231
type PrismaClient,
@@ -1849,22 +1848,19 @@ export class RunEngine {
18491848
organizationId: string;
18501849
tx?: PrismaClientOrTransaction;
18511850
}): Promise<Waitpoint | null> {
1852-
try {
1853-
const waitpoint = await this.runStore.createWaitpoint(
1854-
{
1855-
data: {
1856-
...WaitpointId.generate(),
1857-
type: "BATCH",
1858-
idempotencyKey: batchId,
1859-
userProvidedIdempotencyKey: false,
1860-
completedByBatchId: batchId,
1861-
environmentId,
1862-
projectId,
1863-
},
1864-
},
1865-
tx
1866-
);
1851+
const waitpoint = await this.waitpointSystem.createBatchWaitpoint({
1852+
batchId,
1853+
environmentId,
1854+
projectId,
1855+
tx,
1856+
});
18671857

1858+
// Duplicate batch: the coordinator already reported it.
1859+
if (!waitpoint) {
1860+
return null;
1861+
}
1862+
1863+
try {
18681864
await this.blockRunWithWaitpoint({
18691865
runId,
18701866
waitpoints: waitpoint.id,
@@ -1873,19 +1869,17 @@ export class RunEngine {
18731869
batch: { id: batchId },
18741870
// No tx: the block edge routes to the run's owning DB, not the control-plane tx.
18751871
});
1876-
1877-
return waitpoint;
18781872
} catch (error) {
1879-
if (error instanceof Prisma.PrismaClientKnownRequestError) {
1880-
// duplicate idempotency key
1881-
if (error.code === "P2002") {
1882-
return null;
1883-
} else {
1884-
throw error;
1885-
}
1873+
// The previous shape wrapped the create AND the block in one catch, so a P2002 from
1874+
// the block step also returned null. Kept deliberately: narrowing it here would be a
1875+
// behaviour change smuggled into an extraction.
1876+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
1877+
return null;
18861878
}
18871879
throw error;
18881880
}
1881+
1882+
return waitpoint;
18891883
}
18901884

18911885
async tryCompleteBatch({ batchId }: { batchId: string }): Promise<void> {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ export class WaitpointSystem {
153153
idempotencyKeyExpiresAt?: Date;
154154
}) {
155155
const result = await this.coordinator.createDateTimeWaitpoint({
156+
// Pinned until the mint flag reaches this entry point.
157+
mintKind: "legacy",
156158
runId,
157159
projectId,
158160
environmentId,
@@ -201,6 +203,8 @@ export class WaitpointSystem {
201203
standaloneResidency?: "NEW" | "LEGACY";
202204
}): Promise<{ waitpoint: Waitpoint; isCached: boolean }> {
203205
const result = await this.coordinator.createManualWaitpoint({
206+
// Pinned until the mint flag reaches this entry point.
207+
mintKind: "legacy",
204208
runId,
205209
environmentId,
206210
projectId,
@@ -731,6 +735,21 @@ export class WaitpointSystem {
731735
}); // end of runlock
732736
}
733737

738+
/**
739+
* The BATCH waitpoint for a batch. Returns null when the batch already has one.
740+
*
741+
* mintKind is pinned to legacy until the mint flag is threaded through the batch entry
742+
* point; the store arm is unreachable from here until then.
743+
*/
744+
public async createBatchWaitpoint(params: {
745+
batchId: string;
746+
environmentId: string;
747+
projectId: string;
748+
tx?: PrismaClientOrTransaction;
749+
}): Promise<Waitpoint | null> {
750+
return this.coordinator.createBatchWaitpoint({ ...params, mintKind: "legacy" });
751+
}
752+
734753
public buildRunAssociatedWaitpoint({
735754
projectId,
736755
environmentId,

internal-packages/run-engine/src/engine/waitpointCoordinator/legacyPostgresCoordinator.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { nanoid } from "nanoid";
88
import { UnclassifiableWaitpointId } from "../errors.js";
99
import type {
1010
AssociatedWaitpointData,
11+
CreateBatchWaitpointParams,
1112
ClearRunBlockStateParams,
1213
CompleteParams,
1314
CompleteResult,
@@ -332,6 +333,43 @@ export class LegacyPostgresWaitpointCoordinator implements WaitpointCoordinator
332333
return { kind: "created", waitpoint };
333334
}
334335

336+
/**
337+
* The BATCH waitpoint for a batch, keyed on the batch id as its idempotency key.
338+
*
339+
* The P2002 catch IS the duplicate-batch contract: a second call for the same batch
340+
* collides on the idempotencyKey unique index, and null is the caller's "this batch
341+
* already has one" signal rather than an error. It stays on this arm because the code
342+
* is dead against a non-Postgres store, where NX reports the duplicate instead.
343+
*/
344+
async createBatchWaitpoint({
345+
batchId,
346+
environmentId,
347+
projectId,
348+
tx,
349+
}: CreateBatchWaitpointParams): Promise<Waitpoint | null> {
350+
try {
351+
return await this.runStore.createWaitpoint(
352+
{
353+
data: {
354+
...WaitpointId.generate(),
355+
type: "BATCH",
356+
idempotencyKey: batchId,
357+
userProvidedIdempotencyKey: false,
358+
completedByBatchId: batchId,
359+
environmentId,
360+
projectId,
361+
},
362+
},
363+
tx
364+
);
365+
} catch (error) {
366+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
367+
return null;
368+
}
369+
throw error;
370+
}
371+
}
372+
335373
async createManualWaitpoint({
336374
runId,
337375
environmentId,

internal-packages/run-engine/src/engine/waitpointCoordinator/types.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export type WaitpointCoordinator = {
2424
complete(params: CompleteParams): Promise<CompleteResult>;
2525
createDateTimeWaitpoint(params: CreateDateTimeWaitpointParams): Promise<CreateWaitpointResult>;
2626
createManualWaitpoint(params: CreateManualWaitpointParams): Promise<CreateWaitpointResult>;
27+
createBatchWaitpoint(params: CreateBatchWaitpointParams): Promise<Waitpoint | null>;
2728
mintAssociatedWaitpointData(params: {
2829
projectId: string;
2930
environmentId: string;
@@ -34,6 +35,23 @@ export type WaitpointCoordinator = {
3435
}): Promise<Waitpoint>;
3536
};
3637

38+
/**
39+
* Which coordinator mints a NEW waitpoint. Structurally identical to the webapp's own
40+
* WaitpointMintKind; re-declared because the engine never imports from the webapp.
41+
*
42+
* Read at the mint and never again — every later operation routes by the minted id's shape.
43+
*/
44+
export type WaitpointMintKind = "legacy" | "store";
45+
46+
export type CreateBatchWaitpointParams = {
47+
batchId: string;
48+
environmentId: string;
49+
projectId: string;
50+
mintKind: WaitpointMintKind;
51+
/** Legacy arm only: the create may join a caller transaction. A store arm ignores it. */
52+
tx?: PrismaClientOrTransaction;
53+
};
54+
3755
export type ReadCompletionEnvelopesParams = {
3856
runId: string;
3957
/** The DISTINCT completed waitpoint ids to source. Result order is not meaningful. */
@@ -110,7 +128,15 @@ export type RegisterBlocksParams = {
110128
* The lockless variant writes the edge and does not count. Two methods rather than
111129
* one method with a flag, so "the batch path issues no extra query" is structural.
112130
*/
113-
export type RegisterBlocksLocklessParams = Omit<RegisterBlocksParams, "client">;
131+
export type RegisterBlocksLocklessParams = Omit<RegisterBlocksParams, "client"> & {
132+
/**
133+
* The parent's BATCH waitpoint id. A store arm asserts it is present and PENDING on the
134+
* run's shard before writing any item edge, so the run's pending set can never be
135+
* momentarily empty mid-absorb. Neither TLA+ campaign models this, so the assertion is
136+
* the only protection. A legacy arm ignores it.
137+
*/
138+
batchWaitpointId?: string;
139+
};
114140

115141
export type CompleteParams = {
116142
waitpointId: string;
@@ -143,6 +169,7 @@ export type CreateWaitpointResult =
143169
| { kind: "created"; waitpoint: Waitpoint };
144170

145171
export type CreateDateTimeWaitpointParams = {
172+
mintKind: WaitpointMintKind;
146173
/** When set, the waitpoint co-locates with this run's DB and the dedup probe targets it. */
147174
runId?: string;
148175
projectId: string;
@@ -153,6 +180,7 @@ export type CreateDateTimeWaitpointParams = {
153180
};
154181

155182
export type CreateManualWaitpointParams = {
183+
mintKind: WaitpointMintKind;
156184
runId?: string;
157185
environmentId: string;
158186
projectId: string;

0 commit comments

Comments
 (0)