Skip to content

Commit 0a2ed96

Browse files
committed
fix(run-store): keep the caller transaction on the legacy leg of the edge-delete fan-out
The taskRunId-keyed deleteManyTaskRunWaitpoints fan-out dropped the caller's transaction for both legs. The new (dedicated) leg can't join a control-plane transaction, but the legacy leg can, so pass it through: a legacy run's blocking edges are again deleted atomically with the caller's operation (e.g. an attempt failure) instead of auto-committing, matching the waitpointId-keyed path.
1 parent f0efad3 commit 0a2ed96

2 files changed

Lines changed: 87 additions & 3 deletions

File tree

internal-packages/run-store/src/PostgresRunStore.writeAtomicity.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,86 @@ describe("cross-DB write atomicity (startAttempt + createExecutionSnapshot)", ()
312312
}
313313
);
314314
});
315+
316+
// A run's blocking edges may straddle both DBs mid-drain, so clearBlockingWaitpoints routes the
317+
// taskRunId-keyed delete through the both-stores fan-out. The #new leg can't join a control-plane
318+
// tx, but the #legacy leg CAN — so the caller's tx (e.g. attemptFailed) must still be honored for
319+
// the legacy edges, keeping them atomic with the caller's operation instead of auto-committing.
320+
async function seedLegacyBlockingEdge(
321+
prisma14: PrismaClient,
322+
env: { project: { id: string }; environment: { id: string } },
323+
runId: string,
324+
suffix: string
325+
): Promise<void> {
326+
const waitpoint = await prisma14.waitpoint.create({
327+
data: {
328+
friendlyId: `wp_${suffix}`,
329+
type: "MANUAL",
330+
status: "PENDING",
331+
idempotencyKey: `idem_${suffix}`,
332+
userProvidedIdempotencyKey: false,
333+
projectId: env.project.id,
334+
environmentId: env.environment.id,
335+
},
336+
});
337+
await prisma14.taskRunWaitpoint.create({
338+
data: { taskRunId: runId, waitpointId: waitpoint.id, projectId: env.project.id },
339+
});
340+
}
341+
342+
describe("fan-out deleteManyTaskRunWaitpoints honors the caller's tx on the #legacy leg", () => {
343+
heteroRunOpsPostgresTest(
344+
"rolls the #legacy edge delete back when the caller's control-plane tx rolls back",
345+
async ({ prisma14, prisma17 }) => {
346+
const { router } = makeSplitRouter(prisma14, prisma17);
347+
const env = await seedEnvironment(prisma14, "legacy", "del_tx_rb");
348+
const runId = `run_${CUID_25}`;
349+
await router.createRun(
350+
buildCreateRunInput({
351+
runId,
352+
friendlyId: "run_del_tx_rb",
353+
organizationId: env.organization.id,
354+
projectId: env.project.id,
355+
runtimeEnvironmentId: env.environment.id,
356+
})
357+
);
358+
await seedLegacyBlockingEdge(prisma14, env, runId, "del_tx_rb");
359+
360+
await expect(
361+
prisma14.$transaction(async (tx) => {
362+
await router.deleteManyTaskRunWaitpoints({ where: { taskRunId: runId } }, tx);
363+
throw new Error("rollback");
364+
})
365+
).rejects.toThrow("rollback");
366+
367+
const remaining = await prisma14.taskRunWaitpoint.count({ where: { taskRunId: runId } });
368+
expect(remaining).toBe(1);
369+
}
370+
);
371+
372+
heteroRunOpsPostgresTest(
373+
"still deletes the #legacy edge when the caller's tx commits",
374+
async ({ prisma14, prisma17 }) => {
375+
const { router } = makeSplitRouter(prisma14, prisma17);
376+
const env = await seedEnvironment(prisma14, "legacy", "del_tx_commit");
377+
const runId = `run_${CUID_25}`;
378+
await router.createRun(
379+
buildCreateRunInput({
380+
runId,
381+
friendlyId: "run_del_tx_commit",
382+
organizationId: env.organization.id,
383+
projectId: env.project.id,
384+
runtimeEnvironmentId: env.environment.id,
385+
})
386+
);
387+
await seedLegacyBlockingEdge(prisma14, env, runId, "del_tx_commit");
388+
389+
await prisma14.$transaction(async (tx) => {
390+
await router.deleteManyTaskRunWaitpoints({ where: { taskRunId: runId } }, tx);
391+
});
392+
393+
const remaining = await prisma14.taskRunWaitpoint.count({ where: { taskRunId: runId } });
394+
expect(remaining).toBe(0);
395+
}
396+
);
397+
});

internal-packages/run-store/src/runOpsStore.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,11 +1302,12 @@ export class RoutingRunStore implements RunStore {
13021302
const store = await this.#resolveWaitpointStore(waitpointId);
13031303
return store.deleteManyTaskRunWaitpoints(args, store === this.#legacy ? tx : undefined);
13041304
}
1305-
// Keyed by taskRunId (or other): a run's edges may straddle DBs mid-drain, so delete from
1306-
// both. Can't span one tx across two DBs, so it's dropped for the both-stores path.
1305+
// Keyed by taskRunId (or other): a run's edges may straddle DBs mid-drain, so delete from both.
1306+
// One tx can't span two DBs, so the #new leg is auto-commit; the caller's tx is control-plane, so
1307+
// the #legacy leg keeps it (atomic with the caller's op, matching the waitpointId-keyed path).
13071308
const [fromNew, fromLegacy] = await Promise.all([
13081309
this.#new.deleteManyTaskRunWaitpoints(args),
1309-
this.#legacy.deleteManyTaskRunWaitpoints(args),
1310+
this.#legacy.deleteManyTaskRunWaitpoints(args, tx),
13101311
]);
13111312
return { count: fromNew.count + fromLegacy.count };
13121313
}

0 commit comments

Comments
 (0)