Skip to content

Commit f5a2ea6

Browse files
committed
perf(run-store): route id-set reads to the owning store, not both DBs
Run residency is a total function of the id (run-ops ids resolve to the new store, every other id to legacy), and writes and single-run reads already route by it. The id-set read path was the outlier: it queried the new store for the entire id set, then probed legacy for the misses. While a split is active with most runs still on legacy, that meant a wasted new-store query on every list hydrate and engine sweep. Partition the id set by residency and query each store only for its own ids, in parallel (matching expireRunsBatch). Same result set. The old cross-store fallback existed to prefer a new-store copy of a same-id-in-both collision; that cannot arise when each id maps to exactly one store, so it is removed. The open-predicate path is unchanged (an open where cannot route by id, so it still unions both stores and dedupes).
1 parent 7a14188 commit f5a2ea6

4 files changed

Lines changed: 132 additions & 28 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { heteroPostgresTest } from "@internal/testcontainers";
2+
import type { PrismaClient } from "@trigger.dev/database";
3+
import { classifyResidency } from "@trigger.dev/core/v3/isomorphic";
4+
import { describe, expect } from "vitest";
5+
import { PostgresRunStore } from "./PostgresRunStore.js";
6+
import { RoutingRunStore } from "./runOpsStore.js";
7+
8+
const ORG_ID = "orgroute0000000000000001";
9+
const PROJ_ID = "projroute00000000000001";
10+
const ENV_ID = "envroute0000000000000001";
11+
12+
const newId = (i: number) => "k".repeat(20) + String(i).padStart(4, "0") + "01";
13+
const cuidId = (i: number) => "c".repeat(21) + String(i).padStart(4, "0");
14+
15+
async function seedShared(prisma: PrismaClient, suffix: string) {
16+
await prisma.organization.create({
17+
data: { id: ORG_ID, title: `Route ${suffix}`, slug: `route-${suffix}` },
18+
});
19+
await prisma.project.create({
20+
data: {
21+
id: PROJ_ID,
22+
name: `Route ${suffix}`,
23+
slug: `route-${suffix}`,
24+
externalRef: `proj_route_${suffix}`,
25+
organizationId: ORG_ID,
26+
},
27+
});
28+
await prisma.runtimeEnvironment.create({
29+
data: {
30+
id: ENV_ID,
31+
type: "PRODUCTION",
32+
slug: "prod",
33+
projectId: PROJ_ID,
34+
organizationId: ORG_ID,
35+
apiKey: `tr_prod_${suffix}`,
36+
pkApiKey: `pk_prod_${suffix}`,
37+
shortcode: `short_${suffix}`,
38+
},
39+
});
40+
}
41+
42+
const BASE = new Date("2026-01-01T00:00:00.000Z").getTime();
43+
44+
async function seedRun(prisma: PrismaClient, id: string, offsetSec: number) {
45+
await prisma.taskRun.create({
46+
data: {
47+
id,
48+
engine: "V2",
49+
status: "COMPLETED_SUCCESSFULLY",
50+
friendlyId: `run_${id}`,
51+
runtimeEnvironmentId: ENV_ID,
52+
environmentType: "PRODUCTION",
53+
organizationId: ORG_ID,
54+
projectId: PROJ_ID,
55+
taskIdentifier: "route-task",
56+
payload: "{}",
57+
payloadType: "application/json",
58+
traceId: `trace_${id}`,
59+
spanId: `span_${id}`,
60+
queue: "task/route",
61+
isTest: false,
62+
taskEventStore: "taskEvent",
63+
depth: 0,
64+
createdAt: new Date(BASE + offsetSec * 1000),
65+
},
66+
});
67+
}
68+
69+
describe("RoutingRunStore id-set residency routing", () => {
70+
heteroPostgresTest(
71+
"routes each id to its owning store and merges in orderBy order",
72+
{ timeout: 120000 },
73+
async ({ prisma14, prisma17 }) => {
74+
for (let i = 0; i < 5; i++) {
75+
expect(classifyResidency(newId(i))).toBe("NEW");
76+
expect(classifyResidency(cuidId(i))).toBe("LEGACY");
77+
}
78+
79+
await seedShared(prisma14, "legacy");
80+
await seedShared(prisma17, "new");
81+
82+
for (let i = 0; i < 5; i++) {
83+
await seedRun(prisma17, newId(i), i * 2 + 1);
84+
await seedRun(prisma14, cuidId(i), i * 2);
85+
}
86+
87+
const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 });
88+
const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 });
89+
const router = new RoutingRunStore({ new: newStore, legacy: legacyStore });
90+
91+
const mixedIds = [0, 1, 2, 3, 4].flatMap((i) => [newId(i), cuidId(i)]);
92+
const globalDesc = [newId(4), cuidId(4), newId(3), cuidId(3), newId(2), cuidId(2), newId(1), cuidId(1), newId(0), cuidId(0)];
93+
94+
const all = (await router.findRuns({
95+
where: { id: { in: mixedIds } },
96+
orderBy: { createdAt: "desc" },
97+
take: 100,
98+
})) as Array<{ id: string }>;
99+
expect(all.map((r) => r.id)).toEqual(globalDesc);
100+
101+
const top4 = (await router.findRuns({
102+
where: { id: { in: mixedIds } },
103+
orderBy: { createdAt: "desc" },
104+
take: 4,
105+
})) as Array<{ id: string }>;
106+
expect(top4.map((r) => r.id)).toEqual(globalDesc.slice(0, 4));
107+
108+
const newOnly = (await router.findRuns({
109+
where: { id: { in: [newId(0), newId(2), newId(4)] } },
110+
orderBy: { createdAt: "asc" },
111+
})) as Array<{ id: string }>;
112+
expect(newOnly.map((r) => r.id)).toEqual([newId(0), newId(2), newId(4)]);
113+
114+
const legacyOnly = (await router.findRuns({
115+
where: { id: { in: [cuidId(1), cuidId(3)] } },
116+
orderBy: { createdAt: "asc" },
117+
})) as Array<{ id: string }>;
118+
expect(legacyOnly.map((r) => r.id)).toEqual([cuidId(1), cuidId(3)]);
119+
}
120+
);
121+
});

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,8 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + run-ops id
262262
}
263263
);
264264

265-
// ── Case 1b: NEW-wins on id collision in #findRunsByIdSet ──
266-
// The copy→fence window can leave the same id on both DBs. The id-set path queries NEW first; an id
267-
// already found on NEW must NOT be re-fetched from LEGACY, so the NEW copy wins.
268265
heteroRunOpsPostgresTest(
269-
"case 1b: findRuns by id-set with a colliding id resolves to the NEW copy",
266+
"case 1b: findRuns by id-set routes a cuid id to LEGACY only, ignoring any NEW copy",
270267
async ({ prisma14, prisma17 }) => {
271268
const { router } = makeSplitRouter(prisma14, prisma17);
272269
const env = await seedSharedEnv(prisma14, "m1b");
@@ -304,8 +301,8 @@ describe("RoutingRunStore — mixed-residency matrix (cuid #legacy + run-ops id
304301
where: { id: { in: [collidingId] } },
305302
select: { id: true, taskIdentifier: true },
306303
});
307-
expect(rows).toHaveLength(1); // deduped, not double-reported
308-
expect((rows[0] as any).taskIdentifier).toBe("new-copy-wins"); // NEW wins
304+
expect(rows).toHaveLength(1);
305+
expect((rows[0] as any).taskIdentifier).toBe("my-task");
309306
}
310307
);
311308

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,10 +1004,8 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => {
10041004
}
10051005
);
10061006

1007-
// A run present on BOTH DBs (the copy->fence migration window) must be returned ONCE,
1008-
// and the NEW copy wins.
10091007
heteroPostgresTest(
1010-
"id-set dedupes a run present on both DBs, preferring NEW",
1008+
"id-set routes a cuid id to its LEGACY owner and does not consult NEW",
10111009
async ({ prisma14, prisma17 }) => {
10121010
const legacyStore = new PostgresRunStore({ prisma: prisma14, readOnlyPrisma: prisma14 });
10131011
const newStore = new PostgresRunStore({ prisma: prisma17, readOnlyPrisma: prisma17 });
@@ -1031,7 +1029,7 @@ describe("RoutingRunStore.findRuns split-mode fan-out + drain", () => {
10311029
select: { id: true, taskIdentifier: true },
10321030
})) as Array<{ id: string; taskIdentifier: string }>;
10331031
expect(rows).toHaveLength(1);
1034-
expect(rows[0]!.taskIdentifier).toBe("from-new");
1032+
expect(rows[0]!.taskIdentifier).toBe("from-legacy");
10351033
}
10361034
);
10371035

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

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -324,33 +324,21 @@ export class RoutingRunStore implements RunStore {
324324
return idList ? this.#findRunsByIdSet(args, idList, client) : this.#findRunsOpen(args, client);
325325
}
326326

327-
// Bounded id-set (the list hydrate + engine sweeps). Query NEW for the whole set first
328-
// (it holds run-ops runs); probe LEGACY only for the ids NEW missed that could still live
329-
// there (cuid). The two id sets are disjoint by construction, so the merge needs no dedupe.
330327
async #findRunsByIdSet(
331328
args: FindRunsArgs,
332329
ids: string[],
333330
client?: ReadClient
334331
): Promise<unknown[]> {
335332
const { args: selArgs, addedFields } = ensureProjected(args);
336-
// The id set already bounds the per-store result, so never push take/skip down — doing
337-
// so would truncate a store's page before the merge knows membership and mis-attribute
338-
// rows. take/skip are applied once, globally, in finalizeRows.
339333
const fan = { ...selArgs, take: undefined, skip: undefined };
334+
const newIds = ids.filter((id) => this.#classifySafe(id) === "NEW");
335+
const legacyIds = ids.filter((id) => this.#classifySafe(id) !== "NEW");
340336
const findNew = this.#findManyOn(this.#new, client);
341337
const findLegacy = this.#findManyOn(this.#legacy, client);
342-
343-
const newRows = await findNew(fan);
344-
const foundIds = new Set(newRows.map((r) => r.id as string));
345-
346-
const toLegacy: string[] = [];
347-
for (const id of ids) {
348-
if (foundIds.has(id)) continue;
349-
if (this.#classifySafe(id) === "NEW") continue; // run-ops id: cannot live on LEGACY
350-
toLegacy.push(id);
351-
}
352-
353-
const legacyRows = toLegacy.length > 0 ? await findLegacy(narrowToIds(fan, toLegacy)) : [];
338+
const [newRows, legacyRows] = await Promise.all([
339+
newIds.length > 0 ? findNew(narrowToIds(fan, newIds)) : [],
340+
legacyIds.length > 0 ? findLegacy(narrowToIds(fan, legacyIds)) : [],
341+
]);
354342
return finalizeRows([...newRows, ...legacyRows], args, addedFields);
355343
}
356344

0 commit comments

Comments
 (0)