Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
65a354d
feat(core): mint Postgres waitpoint ids stamped for a gen-2 shard
d-cs Aug 26, 2026
0dae1c5
feat(webapp): carry the shard char through a MintTarget on run inheri…
d-cs Aug 26, 2026
b20792f
feat(webapp): resolve a run's mint target in one place, gated off by …
d-cs Aug 26, 2026
845ab06
fix(webapp): mint a failed child run onto its parent's shard
d-cs Aug 26, 2026
bf32052
feat(webapp): mint a batch id onto its parent run's shard
d-cs Aug 26, 2026
4359bf8
test(run-engine): add a failing census guard for waitpoint mint sites
d-cs Aug 26, 2026
19731d4
feat(run-engine): stamp DATETIME and MANUAL waitpoint ids for the anc…
d-cs Aug 26, 2026
25b2119
feat(run-engine): stamp a run's associated waitpoint id for the run's…
d-cs Aug 26, 2026
6d85a15
feat(run-engine): stamp a BATCH waitpoint id for the batch's shard
d-cs Aug 26, 2026
d28aec2
feat(run-engine,webapp): mint a standalone waitpoint token on the env…
d-cs Aug 26, 2026
13f6124
test(webapp): pin every mint path to today's ids while the shard gate…
d-cs Aug 26, 2026
45ee043
test(run-engine): bind each waitpoint mint site to its anchor, and ma…
d-cs Aug 26, 2026
f9ad14c
fix(webapp): keep the caller's region on an inherited run mint
d-cs Aug 26, 2026
b969f8e
fix(webapp): route a gen-2 batch's completion write to its own shard
d-cs Aug 26, 2026
268b6cd
fix(webapp): return not-found when waiting on a missing waitpoint token
d-cs Aug 26, 2026
0ffb44c
Merge remote-tracking branch 'origin/main' into feat/gen2-minting-tri…
d-cs Aug 27, 2026
46a64d1
perf(core): classify a run-ops id by shape instead of decoding its core
d-cs Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/webapp/app/routes/api.v1.waitpoints.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type PrismaClientOrTransaction,
} from "~/db.server";
import { resolveRunIdMintKind } from "~/v3/engineVersion.server";
import { resolveMintShard } from "~/v3/runOpsMigration/runOpsMintShard.server";
import { logger } from "~/services/logger.server";
import { generateHttpCallbackUrl } from "~/services/httpCallback.server";
import { publicAccessTokenResponseHeaders } from "~/services/publicAccessTokenResponse.server";
Expand Down Expand Up @@ -69,6 +70,16 @@ const { action } = createActionApiRoute(
});
const residency = mintKind === "runOpsId" ? "NEW" : "LEGACY";

// The token's id is minted inside the engine, so the shard travels with the call. No
// extra query: the org flags this reads are already loaded on the authenticated env.
const standaloneShardKey =
mintKind === "runOpsId"
? await resolveMintShard({
id: authentication.environment.id,
orgFeatureFlags: authentication.environment.organization.featureFlags,
})
: undefined;

//upsert tags
let tags: { id: string; name: string }[] = [];
const bodyTags = typeof body.tags === "string" ? [body.tags] : body.tags;
Expand Down Expand Up @@ -101,6 +112,7 @@ const { action } = createActionApiRoute(
timeout,
tags: bodyTags,
standaloneResidency: residency,
standaloneShardKey,
Comment thread
d-cs marked this conversation as resolved.
});

const waitpointId = WaitpointId.toFriendlyId(result.waitpoint.id);
Expand Down
21 changes: 10 additions & 11 deletions apps/webapp/app/runEngine/services/triggerFailedTask.server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { RunEngine } from "@internal/run-engine";
import { TaskRunErrorCodes, type TaskRunError } from "@trigger.dev/core/v3";
import { RunId, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic";
import { RunId } from "@trigger.dev/core/v3/isomorphic";
import type {
PrismaClientOrTransaction,
RuntimeEnvironmentType,
TaskRun,
} from "@trigger.dev/database";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { resolveRunIdMintKind } from "~/v3/engineVersion.server";
import { resolveInheritedMintKind } from "~/v3/runOpsMigration/resolveInheritedMintKind.server";
import { mintFriendlyIdForKind } from "~/v3/runOpsMigration/mintAnchoredRunFriendlyId.server";
import { resolveRunMintTarget } from "~/v3/runOpsMigration/resolveRunMintTarget.server";
import { getEventRepository } from "~/v3/eventRepository/index.server";
import { runStore as defaultRunStore } from "~/v3/runStore.server";
import type { RunStore } from "@internal/run-store";
Expand Down Expand Up @@ -103,17 +103,16 @@ export class TriggerFailedTaskService {
return args.runFriendlyId;
}

const mintKind = args.parentRunFriendlyId
? resolveInheritedMintKind(args.parentRunFriendlyId)
: await resolveRunIdMintKind({
return mintFriendlyIdForKind(
await resolveRunMintTarget({
environment: {
organizationId: args.organizationId,
id: args.environmentId,
orgFeatureFlags: args.orgFeatureFlags,
});

return mintKind === "runOpsId"
? RunId.toFriendlyId(generateRunOpsId())
: RunId.generate().friendlyId;
},
parentRunFriendlyId: args.parentRunFriendlyId,
})
);
}

async call(request: TriggerFailedTaskRequest): Promise<string | null> {
Expand Down
17 changes: 9 additions & 8 deletions apps/webapp/app/runEngine/services/triggerTask.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ import { parseDelay } from "~/utils/delays";
import { removeNullBytesFromKey } from "~/utils/nullBytes";
import { handleMetadataPacket } from "~/utils/packets";
import { startSpan } from "~/v3/tracing.server";
import { resolveRunIdMintKind } from "~/v3/engineVersion.server";
import { resolveInheritedMintKind } from "~/v3/runOpsMigration/resolveInheritedMintKind.server";
import { mintFriendlyIdForKind } from "~/v3/runOpsMigration/mintAnchoredRunFriendlyId.server";
import { resolveRunMintTarget } from "~/v3/runOpsMigration/resolveRunMintTarget.server";
import type {
TriggerTaskServiceOptions,
TriggerTaskServiceResult,
Expand Down Expand Up @@ -218,15 +217,17 @@ export class RunEngineTriggerTaskService {
parentRunFriendlyId?: string,
region?: string
): Promise<string> {
const mintKind = parentRunFriendlyId
? resolveInheritedMintKind(parentRunFriendlyId)
: await resolveRunIdMintKind({
return mintFriendlyIdForKind(
await resolveRunMintTarget({
environment: {
organizationId: environment.organizationId,
id: environment.id,
orgFeatureFlags: environment.organization.featureFlags,
});

return mintFriendlyIdForKind(mintKind, region);
},
parentRunFriendlyId,
region,
})
);
}

public async call({
Expand Down
2 changes: 2 additions & 0 deletions apps/webapp/app/v3/runEngineHandlers.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
runOpsNewPrismaClient,
runOpsNewReplicaClient,
runOpsLegacyPrismaClient,
runOpsShardHandles,
} from "~/db.server";
import { env } from "~/env.server";
import { findEnvironmentById, findEnvironmentFromRun } from "~/models/runtimeEnvironment.server";
Expand Down Expand Up @@ -1060,6 +1061,7 @@ export function setupBatchQueueCallbacks() {
newReplica: runOpsNewReplicaClient,
newWriter: runOpsNewPrismaClient,
legacyWriter: runOpsLegacyPrismaClient,
shards: runOpsShardHandles,
tryCompleteBatch: (batchId) => engine.tryCompleteBatch({ batchId }),
});
});
Expand Down
20 changes: 20 additions & 0 deletions apps/webapp/app/v3/runEngineHandlersShared.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* whole webapp service graph). The handlers wire the production defaults; tests
* inject per-container stores/replicas, so these helpers never import db.server.
*/
import { resolveShard } from "@trigger.dev/core/v3/isomorphic";
import type { CompleteBatchResult } from "@internal/run-engine";
import type { RunOpsPrismaClient } from "@internal/run-ops-database";
import type { RunStore } from "@internal/run-store";
Expand Down Expand Up @@ -83,8 +84,25 @@ export async function resolveBatchRunOpsWriter(
newReplica: RunOpsPrismaClient;
newWriter: RunOpsPrismaClient;
legacyWriter: RunOpsPrismaClient;
shards?: ReadonlyArray<{ key: string; writer: RunOpsPrismaClient }>;
}
): Promise<RunOpsPrismaClient> {
// A gen-2 batch names its own shard in its id, so route by that and never probe. The
// probe below is binary — NEW, else assume LEGACY — so a gen-2 batch would fall through
// to a store that holds no such row, and the completion update would throw before the
// batch waitpoint could complete, leaving the parent run blocked with nothing logged.
const shardKey = resolveShard(batchId);
if (shardKey !== "new" && shardKey !== "legacy") {
const shard = deps.shards?.find((s) => s.key === shardKey);
if (!shard) {
// Writing to a guessed store is what strands a run. Fail loud instead.
throw new Error(
`resolveBatchRunOpsWriter: batch "${batchId}" names shard "${shardKey}", which is not configured`
);
}
return shard.writer;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

const onNew = await deps.newReplica.batchTaskRun.findFirst({
where: { id: batchId },
select: { id: true },
Expand All @@ -106,6 +124,7 @@ export type BatchCompletionDeps = {
newReplica: RunOpsPrismaClient;
newWriter: RunOpsPrismaClient;
legacyWriter: RunOpsPrismaClient;
shards?: ReadonlyArray<{ key: string; writer: RunOpsPrismaClient }>;
tryCompleteBatch: (batchId: string) => Promise<unknown>;
};

Expand Down Expand Up @@ -136,6 +155,7 @@ export async function handleBatchCompletion(
newReplica: deps.newReplica,
newWriter: deps.newWriter,
legacyWriter: deps.legacyWriter,
shards: deps.shards,
});

try {
Expand Down
114 changes: 114 additions & 0 deletions apps/webapp/app/v3/runOpsMigration/gen2MintInertness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { describe, expect, it, vi } from "vitest";
import { classifyKind, mintWaitpointIdFor, resolveShard } from "@trigger.dev/core/v3/isomorphic";
import { resolveInheritedMintKind } from "./resolveInheritedMintKind.server";
import {
mintAnchoredRunFriendlyId,
mintFriendlyIdForKind,
} from "./mintAnchoredRunFriendlyId.server";
import { batchIdForMintKind } from "./mintBatchFriendlyId.server";
import { resolveRunMintTarget } from "./resolveRunMintTarget.server";

// The gate is off when RUN_OPS_SHARDS is unset OR runOpsMintShardSet is empty. Either way
// resolveMintShard answers "new", so no shard char reaches a MintTarget. Every assertion
// below is "the id is what it was before gen-2 existed".
const offShard = vi.fn().mockResolvedValue("new" as const);
const environment = { organizationId: "org_1", id: "env_1", orgFeatureFlags: {} };

describe("gate off — run mint paths", () => {
it("a root run on the run-ops path mints a gen-1 v1 id", async () => {
const target = await resolveRunMintTarget({
environment,
region: "us-east-1",
deps: {
resolveRunIdMintKind: vi.fn().mockResolvedValue("runOpsId"),
resolveMintShard: offShard,
},
});
const body = mintFriendlyIdForKind(target).slice(4);
expect(body.length).toBe(26);
expect(body[24]).toBe("e"); // the region char, as today
expect(body[25]).toBe("1");
});

it("a root run on a non-cut-over org mints a cuid", async () => {
const target = await resolveRunMintTarget({
environment,
deps: {
resolveRunIdMintKind: vi.fn().mockResolvedValue("cuid"),
resolveMintShard: offShard,
},
});
expect(mintFriendlyIdForKind(target).slice(4).length).toBe(25);
});

it("a child of a gen-1 parent keeps the caller's region char", async () => {
// The pre-split code passed the region on BOTH arms, so a child run stamped the
// requested region. Dropping it on the inherited arm would silently stamp the default.
const target = await resolveRunMintTarget({
environment,
parentRunFriendlyId: `run_${"a".repeat(24)}01`,
region: "us-east-1",
deps: {
resolveRunIdMintKind: vi.fn().mockResolvedValue("runOpsId"),
resolveMintShard: offShard,
},
});
const body = mintFriendlyIdForKind(target).slice(4);
expect(body[24]).toBe("e");
expect(body[25]).toBe("1");
});

it("a gen-2 parent's shard still outranks the caller's region", async () => {
const target = await resolveRunMintTarget({
environment,
parentRunFriendlyId: `run_${"a".repeat(24)}a2`,
region: "us-east-1",
deps: {
resolveRunIdMintKind: vi.fn().mockResolvedValue("runOpsId"),
resolveMintShard: offShard,
},
});
expect(mintFriendlyIdForKind(target).slice(4)[24]).toBe("a");
});

it("a child of a gen-1 parent mints a gen-1 v1 id", () => {
const body = mintFriendlyIdForKind(resolveInheritedMintKind(`run_${"a".repeat(24)}01`)).slice(
4
);
expect(body[25]).toBe("1");
});

it("a child of a cuid parent mints a cuid", () => {
expect(
mintFriendlyIdForKind(resolveInheritedMintKind(`run_${"b".repeat(25)}`)).slice(4).length
).toBe(25);
});
});

describe("gate off — batch and item paths", () => {
it("a batch with no shard char mints a gen-1 v1 id", () => {
const r = batchIdForMintKind({ kind: "runOpsId" });
expect(r.id.length).toBe(26);
expect(r.id[25]).toBe("1");
expect(classifyKind(r.id)).toBe("runOpsId");
});

it("a batch on a non-cut-over org mints a cuid", () => {
expect(batchIdForMintKind({ kind: "cuid" }).id.length).toBe(25);
});

it("a batch item anchored on a gen-1 batch mints a gen-1 v1 id", () => {
const body = mintAnchoredRunFriendlyId(`batch_${"a".repeat(24)}01`).slice(4);
expect(body[25]).toBe("1");
});
});

describe("gate off — waitpoint paths", () => {
it("every gen-1 or legacy anchor yields a cuid waitpoint id", () => {
for (const anchor of [`${"a".repeat(24)}01`, "c".repeat(25), undefined]) {
const r = mintWaitpointIdFor(anchor);
expect(r.id.length).toBe(25);
expect(resolveShard(r.id)).toBe("legacy");
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,16 @@ describe("mintAnchoredRunFriendlyId", () => {
expect(parsed.format).toBe("b32hex");
expect(parsed.format === "b32hex" && parsed.region).toBe(REGION_CODES["us-east-1"]);
});

it("a gen-2 batch anchor mints an item on the batch's shard", () => {
const body = mintAnchoredRunFriendlyId(`batch_${"a".repeat(24)}a2`).slice("run_".length);
expect(body).toHaveLength(26);
expect(body[24]).toBe("a");
expect(body[25]).toBe("2");
});

it("a gen-2 batch anchor ignores a caller region: the shard owns index 24", () => {
const body = mintAnchoredRunFriendlyId(`batch_${"a".repeat(24)}a2`, "us-east-1").slice(4);
expect(body[24]).toBe("a");
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { generateRunOpsId, RunId, type ResidencyKind } from "@trigger.dev/core/v3/isomorphic";
import { generateRunOpsId, generateRunOpsIdV2, RunId } from "@trigger.dev/core/v3/isomorphic";
import type { MintTarget } from "./mintTarget";
import { resolveInheritedMintKind } from "./resolveInheritedMintKind.server";

// Shared id-generation branch for every run-mint path: "runOpsId" -> NEW store, "cuid" -> LEGACY.
export function mintFriendlyIdForKind(mintKind: ResidencyKind, region?: string): string {
return mintKind === "runOpsId"
? RunId.toFriendlyId(generateRunOpsId(region))
: RunId.generate().friendlyId;
// Shared id-generation branch for every run-mint path: "runOpsId" -> a dedicated store,
// "cuid" -> LEGACY. A shardChar selects one gen-2 shard and takes index 24; without one,
// the region takes that slot exactly as it does today.
export function mintFriendlyIdForKind(target: MintTarget): string {
if (target.kind !== "runOpsId") {
return RunId.generate().friendlyId;
}

return RunId.toFriendlyId(
target.shardChar ? generateRunOpsIdV2(target.shardChar) : generateRunOpsId(target.region)
);
}

// Anchor a batch item's mint on the BATCH's friendlyId (id-shape, zero I/O), never the per-org
// flag, so the item and its BatchTaskRun stay co-resident across a mid-batch flag flip.
export function mintAnchoredRunFriendlyId(batchFriendlyId: string, region?: string): string {
return mintFriendlyIdForKind(resolveInheritedMintKind(batchFriendlyId), region);
return mintFriendlyIdForKind({ ...resolveInheritedMintKind(batchFriendlyId), region });
}
Loading
Loading