Skip to content

Commit 4021096

Browse files
committed
fix(webapp): anchor batch item run-ops residency to the batch friendlyId
On the v4 (RunEngine V2) batch paths, RunEngineBatchTriggerService (api.v2) and the BatchQueue item callback (api.v3), a parentless batch's item runs minted their run-ops residency from a fresh per-org flag read at processing time. A mint-flag flip between batch creation and item processing could then mint an item into a different physical store than its BatchTaskRun row: an FK violation against the run-ops DB's live TaskRun_batchId_fkey (batch on legacy, item on new), or a silent orphan (batch on new, item on legacy). Anchor each item's mint on the batch's own friendlyId (pure id-shape, zero new queries), mirroring the already-safe BatchTriggerV3Service, and anchor the pre-failed-run fallback the same way since it also sets batchId. Consolidate the shared id-generation branch into mintFriendlyIdForKind so every mint path stays in lockstep. Single-DB mode is unchanged: a cuid-shaped batch friendlyId yields a cuid item.
1 parent 4c2c255 commit 4021096

11 files changed

Lines changed: 509 additions & 10 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Anchor batch item run-ops residency on the batch's own friendlyId (not a fresh per-org flag read) in the run-engine batch trigger service and the BatchQueue item callback, on both the success path and the pre-failed-run failure path, so a mid-batch mint-flag flip can no longer mint an item (or a pre-failed item) into a different physical store than its BatchTaskRun row.

apps/webapp/app/runEngine/services/batchTrigger.server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
1818
import { logger } from "~/services/logger.server";
1919
import { batchTriggerWorker } from "~/v3/batchTriggerWorker.server";
2020
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
21+
import { mintAnchoredRunFriendlyId } from "~/v3/runOpsMigration/mintAnchoredRunFriendlyId.server";
2122
import { mintBatchFriendlyId } from "~/v3/runOpsMigration/mintBatchFriendlyId.server";
2223
import {
2324
downloadPacketFromObjectStore,
@@ -588,6 +589,9 @@ export class RunEngineBatchTriggerService extends WithRunEngine {
588589
parentRunId,
589590
resumeParentOnCompletion,
590591
batch: { id: batch.id, index: workingIndex },
592+
// Anchor the pre-failed run on the BATCH's residency (same as the happy path) so it
593+
// co-resides with its BatchTaskRun row regardless of a mid-batch mint-flag flip.
594+
runFriendlyId: mintAnchoredRunFriendlyId(batch.friendlyId, item.options?.region),
591595
options: item.options as Record<string, unknown>,
592596
traceContext: options?.traceContext as Record<string, unknown> | undefined,
593597
spanParentAsLink: options?.spanParentAsLink,
@@ -677,6 +681,12 @@ export class RunEngineBatchTriggerService extends WithRunEngine {
677681

678682
const triggerTaskService = new TriggerTaskService();
679683

684+
// Anchor the item's mint on the BATCH's own friendlyId (not a fresh per-org flag read) so
685+
// an org's mint flag flipping between batch creation and this (possibly much later,
686+
// worker-processed) item never splits the item from its BatchTaskRun row. See
687+
// mintAnchoredRunFriendlyId.server.ts.
688+
const runFriendlyId = mintAnchoredRunFriendlyId(batch.friendlyId, item.options?.region);
689+
680690
const result = await triggerTaskService.call(
681691
item.task,
682692
environment,
@@ -695,6 +705,7 @@ export class RunEngineBatchTriggerService extends WithRunEngine {
695705
spanParentAsLink: options?.spanParentAsLink,
696706
batchId: batch.id,
697707
batchIndex: currentIndex,
708+
runFriendlyId,
698709
realtimeStreamsVersion: options?.realtimeStreamsVersion,
699710
triggerSource: options?.triggerSource ?? "api",
700711
triggerAction: options?.triggerAction ?? "trigger",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
// Empty singletons satisfy the module-level wiring imports; the mint method under test is driven
4+
// directly via (service as any) and never touches the DB (same boundary as triggerTask.server.test.ts).
5+
vi.mock("~/db.server", () => ({
6+
prisma: {},
7+
$replica: {},
8+
runOpsNewPrisma: {},
9+
runOpsLegacyPrisma: {},
10+
runOpsNewReplica: {},
11+
runOpsLegacyReplica: {},
12+
}));
13+
vi.mock("~/v3/runOpsMigration/splitMode.server", () => ({ isSplitEnabled: async () => false }));
14+
15+
import { classifyKind, generateRunOpsId, RunId } from "@trigger.dev/core/v3/isomorphic";
16+
import { TriggerFailedTaskService } from "./triggerFailedTask.server";
17+
18+
function buildService() {
19+
return new TriggerFailedTaskService({ prisma: {} as any, engine: {} as any });
20+
}
21+
22+
describe("TriggerFailedTaskService.mintFailedRunFriendlyId", () => {
23+
it("returns the caller-supplied runFriendlyId verbatim (override wins over any mint)", async () => {
24+
const override = RunId.toFriendlyId(generateRunOpsId());
25+
const minted = await (buildService() as any).mintFailedRunFriendlyId({
26+
organizationId: "org_1",
27+
environmentId: "env_1",
28+
runFriendlyId: override,
29+
});
30+
expect(minted).toBe(override);
31+
});
32+
33+
it("without an override, still inherits a run-ops (NEW) parent by id-shape", async () => {
34+
const parentRunFriendlyId = RunId.toFriendlyId(generateRunOpsId());
35+
const minted = await (buildService() as any).mintFailedRunFriendlyId({
36+
organizationId: "org_1",
37+
environmentId: "env_1",
38+
parentRunFriendlyId,
39+
});
40+
expect(classifyKind(minted)).toBe("runOpsId");
41+
});
42+
});

apps/webapp/app/runEngine/services/triggerFailedTask.server.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export type TriggerFailedTaskRequest = {
4343
spanParentAsLink?: boolean;
4444

4545
errorCode?: TaskRunErrorCodes;
46+
47+
/** Pre-minted friendlyId; when set it wins over the mint. Batch callers pass a batch-anchored id. */
48+
runFriendlyId?: string;
4649
};
4750

4851
/**
@@ -86,14 +89,20 @@ export class TriggerFailedTaskService {
8689

8790
// Mint a failed run's friendlyId. The id-kind decides which store the run is
8891
// born in (cuid → legacy store, run-ops id → new store); the whole subgraph of a
89-
// run must agree. Root failed runs mint by the environment's setting; child
90-
// failed runs inherit the parent's current store so they never split.
92+
// run must agree. A caller-supplied runFriendlyId (batch-anchored id) wins verbatim;
93+
// otherwise root failed runs mint by the environment's setting and child failed runs
94+
// inherit the parent's current store so they never split.
9195
private async mintFailedRunFriendlyId(args: {
9296
organizationId: string;
9397
environmentId: string;
9498
orgFeatureFlags?: unknown;
9599
parentRunFriendlyId?: string;
100+
runFriendlyId?: string;
96101
}): Promise<string> {
102+
if (args.runFriendlyId) {
103+
return args.runFriendlyId;
104+
}
105+
97106
const mintKind = args.parentRunFriendlyId
98107
? resolveInheritedMintKind(args.parentRunFriendlyId)
99108
: await resolveRunIdMintKind({
@@ -125,6 +134,7 @@ export class TriggerFailedTaskService {
125134
environmentId: request.environment.id,
126135
orgFeatureFlags: request.environment.organization.featureFlags,
127136
parentRunFriendlyId: request.parentRunId,
137+
runFriendlyId: request.runFriendlyId,
128138
});
129139
mintedFriendlyId = failedRunFriendlyId;
130140

apps/webapp/app/runEngine/services/triggerTask.server.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
TriggerTraceContext,
1515
} from "@trigger.dev/core/v3";
1616
import {
17-
generateRunOpsId,
1817
parseTraceparent,
1918
RunId,
2019
serializeTraceparent,
@@ -28,6 +27,7 @@ import { handleMetadataPacket } from "~/utils/packets";
2827
import { startSpan } from "~/v3/tracing.server";
2928
import { resolveRunIdMintKind } from "~/v3/engineVersion.server";
3029
import { resolveInheritedMintKind } from "~/v3/runOpsMigration/resolveInheritedMintKind.server";
30+
import { mintFriendlyIdForKind } from "~/v3/runOpsMigration/mintAnchoredRunFriendlyId.server";
3131
import type {
3232
TriggerTaskServiceOptions,
3333
TriggerTaskServiceResult,
@@ -153,9 +153,7 @@ export class RunEngineTriggerTaskService {
153153
orgFeatureFlags: environment.organization.featureFlags,
154154
});
155155

156-
return mintKind === "runOpsId"
157-
? RunId.toFriendlyId(generateRunOpsId(region))
158-
: RunId.generate().friendlyId;
156+
return mintFriendlyIdForKind(mintKind, region);
159157
}
160158

161159
public async call({

apps/webapp/app/v3/runEngineHandlers.server.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { getEventRepositoryForStore, recordRunDebugLog } from "./eventRepository
2828
import { roomFromFriendlyRunId, socketIo } from "./handleSocketIo.server";
2929
import { engine } from "./runEngine.server";
3030
import { runStore } from "./runStore.server";
31+
import { mintAnchoredRunFriendlyId } from "~/v3/runOpsMigration/mintAnchoredRunFriendlyId.server";
3132
import { isSplitEnabled } from "~/v3/runOpsMigration/splitMode.server";
3233
import { PerformTaskRunAlertsService } from "./services/alerts/performTaskRunAlerts.server";
3334
import {
@@ -837,6 +838,12 @@ export function setupBatchQueueCallbacks() {
837838
parentRunId: meta.parentRunId,
838839
resumeParentOnCompletion: meta.resumeParentOnCompletion,
839840
batch: { id: batchId, index: itemIndex },
841+
// Anchor the pre-failed run on the BATCH's residency so it co-resides with its
842+
// BatchTaskRun row regardless of a mid-batch mint-flag flip.
843+
runFriendlyId: mintAnchoredRunFriendlyId(
844+
friendlyId,
845+
(item.options as { region?: string } | undefined)?.region
846+
),
840847
traceContext: meta.traceContext as Record<string, unknown> | undefined,
841848
spanParentAsLink: meta.spanParentAsLink,
842849
});
@@ -874,6 +881,14 @@ export function setupBatchQueueCallbacks() {
874881
// Normalize payload - for application/store (R2 paths), this passes through as-is
875882
const payload = normalizePayload(item.payload, item.payloadType);
876883

884+
// Anchor the item's mint on the BATCH's own friendlyId (not a fresh per-org flag
885+
// read) so an org's mint flag flipping between batch creation and this queue-driven
886+
// (possibly much later) callback never splits the item from its BatchTaskRun row.
887+
const runFriendlyId = mintAnchoredRunFriendlyId(
888+
friendlyId,
889+
(item.options as { region?: string } | undefined)?.region
890+
);
891+
877892
const result = await triggerTaskService.call(
878893
item.task,
879894
environment,
@@ -893,6 +908,7 @@ export function setupBatchQueueCallbacks() {
893908
spanParentAsLink: meta.spanParentAsLink,
894909
batchId,
895910
batchIndex: itemIndex,
911+
runFriendlyId,
896912
realtimeStreamsVersion: meta.realtimeStreamsVersion,
897913
planType: meta.planType,
898914
triggerSource: meta.parentRunId ? "sdk" : (meta.triggerSource ?? "api"),
@@ -929,6 +945,10 @@ export function setupBatchQueueCallbacks() {
929945
parentRunId: meta.parentRunId,
930946
resumeParentOnCompletion: meta.resumeParentOnCompletion,
931947
batch: { id: batchId, index: itemIndex },
948+
runFriendlyId: mintAnchoredRunFriendlyId(
949+
friendlyId,
950+
(item.options as { region?: string } | undefined)?.region
951+
),
932952
options: item.options as Record<string, unknown>,
933953
traceContext: meta.traceContext as Record<string, unknown> | undefined,
934954
spanParentAsLink: meta.spanParentAsLink,
@@ -1009,6 +1029,10 @@ export function setupBatchQueueCallbacks() {
10091029
parentRunId: meta.parentRunId,
10101030
resumeParentOnCompletion: meta.resumeParentOnCompletion,
10111031
batch: { id: batchId, index: itemIndex },
1032+
runFriendlyId: mintAnchoredRunFriendlyId(
1033+
friendlyId,
1034+
(item.options as { region?: string } | undefined)?.region
1035+
),
10121036
options: item.options as Record<string, unknown>,
10131037
traceContext: meta.traceContext as Record<string, unknown> | undefined,
10141038
spanParentAsLink: meta.spanParentAsLink,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
BatchId,
3+
classifyKind,
4+
generateRunOpsId,
5+
parseRunId,
6+
REGION_CODES,
7+
} from "@trigger.dev/core/v3/isomorphic";
8+
import { describe, expect, it } from "vitest";
9+
import { mintAnchoredRunFriendlyId } from "./mintAnchoredRunFriendlyId.server";
10+
11+
describe("mintAnchoredRunFriendlyId", () => {
12+
it("a run-ops (NEW) batch anchor yields a run-ops (NEW) item friendlyId", () => {
13+
const batchFriendlyId = BatchId.toFriendlyId(generateRunOpsId());
14+
const itemFriendlyId = mintAnchoredRunFriendlyId(batchFriendlyId);
15+
expect(classifyKind(itemFriendlyId)).toBe("runOpsId");
16+
});
17+
18+
it("a cuid (LEGACY) batch anchor yields a cuid (LEGACY) item friendlyId", () => {
19+
const batchFriendlyId = BatchId.generate().friendlyId;
20+
const itemFriendlyId = mintAnchoredRunFriendlyId(batchFriendlyId);
21+
expect(classifyKind(itemFriendlyId)).toBe("cuid");
22+
});
23+
24+
it("stamps the requested region char into a run-ops id", () => {
25+
const batchFriendlyId = BatchId.toFriendlyId(generateRunOpsId());
26+
const itemFriendlyId = mintAnchoredRunFriendlyId(batchFriendlyId, "us-east-1");
27+
const parsed = parseRunId(itemFriendlyId);
28+
expect(parsed.format).toBe("b32hex");
29+
expect(parsed.format === "b32hex" && parsed.region).toBe(REGION_CODES["us-east-1"]);
30+
});
31+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { generateRunOpsId, RunId, type ResidencyKind } from "@trigger.dev/core/v3/isomorphic";
2+
import { resolveInheritedMintKind } from "./resolveInheritedMintKind.server";
3+
4+
// Shared id-generation branch for every run-mint path: "runOpsId" -> NEW store, "cuid" -> LEGACY.
5+
export function mintFriendlyIdForKind(mintKind: ResidencyKind, region?: string): string {
6+
return mintKind === "runOpsId"
7+
? RunId.toFriendlyId(generateRunOpsId(region))
8+
: RunId.generate().friendlyId;
9+
}
10+
11+
// Anchor a batch item's mint on the BATCH's friendlyId (id-shape, zero I/O), never the per-org
12+
// flag, so the item and its BatchTaskRun stay co-resident across a mid-batch flag flip.
13+
export function mintAnchoredRunFriendlyId(batchFriendlyId: string, region?: string): string {
14+
return mintFriendlyIdForKind(resolveInheritedMintKind(batchFriendlyId), region);
15+
}

apps/webapp/app/v3/services/batchTriggerV3.server.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
Prisma,
1313
} from "@trigger.dev/database";
1414
import type { RunStore } from "@internal/run-store";
15-
import { generateRunOpsId, RunId } from "@trigger.dev/core/v3/isomorphic";
1615
import { z } from "zod";
1716
import type { PrismaClientOrTransaction } from "~/db.server";
1817
import { prisma } from "~/db.server";
@@ -26,6 +25,7 @@ import { getEntitlement } from "~/services/platform.v3.server";
2625
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
2726
import { resolveRunIdMintKind, type RunIdMintKind } from "~/v3/engineVersion.server";
2827
import { resolveInheritedMintKind } from "~/v3/runOpsMigration/resolveInheritedMintKind.server";
28+
import { mintFriendlyIdForKind } from "~/v3/runOpsMigration/mintAnchoredRunFriendlyId.server";
2929
import { mintBatchFriendlyId } from "~/v3/runOpsMigration/mintBatchFriendlyId.server";
3030
import { batchTriggerWorker } from "../batchTriggerWorker.server";
3131
import { legacyRunEngineWorker } from "../legacyRunEngineWorker.server";
@@ -361,9 +361,7 @@ export class BatchTriggerV3Service extends BaseService {
361361
orgFeatureFlags: environment.organization.featureFlags,
362362
});
363363

364-
return mintKind === "runOpsId"
365-
? RunId.toFriendlyId(generateRunOpsId(region))
366-
: RunId.generate().friendlyId;
364+
return mintFriendlyIdForKind(mintKind, region);
367365
}
368366

369367
async #prepareRunData(

0 commit comments

Comments
 (0)