Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fa27151
feat(run-ops): run-store routing seam + run-engine read seams
d-cs Jul 1, 2026
cd171bc
test(run-ops split): mint ksuid explicitly in triggerCreateRouting ro…
d-cs Jul 1, 2026
2ccfa85
test(run-ops): drop dev-process enumeration/fix labels from batchSyst…
d-cs Jul 2, 2026
668ef2c
refactor(run-ops): remove the redirect-marker fencing primitive
d-cs Jul 2, 2026
0d30c94
test(run-ops): drop dev-process labels from run-engine test comments;…
d-cs Jul 2, 2026
756971a
refactor(run-store): rename GroupAHydrator→DedicatedRelationHydrator;…
d-cs Jul 2, 2026
29dbaf7
test(run-ops): strip dev enumeration label from dequeue recovery test…
d-cs Jul 2, 2026
eb96ac8
refactor(run-store): rename GroupA relation identifiers to Dedicated
d-cs Jul 2, 2026
4051bdd
style(run-ops): apply oxfmt
d-cs Jul 2, 2026
3cc5410
fix(run-ops split): route read-your-writes to the owning store's writ…
d-cs Jul 2, 2026
233ca29
fix(run-ops split): normalize run-ops-generation Prisma errors to the…
d-cs Jul 2, 2026
10e0bb7
fix(run-ops split): resolve NEW-resident batches in ApiBatchResultsPr…
d-cs Jul 2, 2026
bcaefdb
build(run-ops): sync pnpm-lock with run-store run-ops-database depend…
d-cs Jul 2, 2026
aff5744
fix(run-ops split): don't drop queue ack/waitpoint completion when en…
d-cs Jul 2, 2026
aad0079
style(run-ops): apply oxfmt to PostgresRunStore
d-cs Jul 2, 2026
622ae49
test(run-ops split): assert expireRun completes the run when resolveE…
d-cs Jul 2, 2026
cdf3d15
chore: add server-changes for pr04
d-cs Jul 3, 2026
58f9232
chore(run-ops): fix lint/format for main lint rules
d-cs Jul 3, 2026
d46aab2
fix(run-ops split): make passthrough assertEnvExists a no-op in singl…
d-cs Jul 3, 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
6 changes: 6 additions & 0 deletions .server-changes/run-engine-store-routing-seam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Route run engine lifecycle operations through the run store and a control-plane resolver so run data can live on a dedicated backing store separate from the control plane.
23 changes: 13 additions & 10 deletions apps/webapp/app/presenters/v3/ApiBatchResultsPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ export class ApiBatchResultsPresenter extends BasePresenter {
env: AuthenticatedEnvironment
): Promise<BatchTaskRunExecutionResult | undefined> {
return this.traceWithEnv("call", env, async (span) => {
const batchRun = await this._prisma.batchTaskRun.findFirst({
where: {
friendlyId,
runtimeEnvironmentId: env.id,
},
include: {
items: {
select: {
taskRunId: true,
// Route through the store so a NEW-resident batch resolves under the run-ops split (the
// router probes NEW→LEGACY and drops this client hint) instead of 404ing on a control-plane read.
const batchRun = await runStore.findBatchTaskRunByFriendlyId(
friendlyId,
env.id,
{
include: {
items: {
select: {
taskRunId: true,
},
},
},
},
});
this._prisma
);

if (!batchRun) {
return undefined;
Expand Down
97 changes: 97 additions & 0 deletions apps/webapp/test/presenters/ApiBatchResultsPresenter.split.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Run-ops split resolution LOCK for ApiBatchResultsPresenter.
//
// GET /api/v1/batches/:id/results constructs the presenter BARE (no injected client), so it must
// resolve a batch that lives in the NEW run-ops DB on its own. The presenter routes the batch-row
// lookup through the `runStore` singleton, whose split router probes NEW→LEGACY. This drives a
// NEW-resident (ksuid) batch through a REAL two-physical-DB split router and asserts the bare
// presenter finds it. Fails before the fix (the presenter read the control-plane DB directly and
// 404'd on a NEW-resident batch).

import { heteroRunOpsPostgresTest } from "@internal/testcontainers";
import { PostgresRunStore, RoutingRunStore } from "@internal/run-store";
import type { RunOpsPrismaClient } from "@internal/run-ops-database";
import type { Organization, PrismaClient, Project } from "@trigger.dev/database";
import { generateKsuidId } from "@trigger.dev/core/v3/isomorphic";
import { expect, vi } from "vitest";
import { ApiBatchResultsPresenter } from "~/presenters/v3/ApiBatchResultsPresenter.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";

// The split router built over the two testcontainer DBs; injected in place of the db.server-backed
// singleton the presenter imports. Populated per-test before the presenter is constructed.
let testRunStore: RoutingRunStore;

// Presenter reads the batch row via `runStore`; child-run reads also go through it. Neutralize the
// real db.server singleton (no env DB) and the runStore singleton (use the split router below).
// The getter defers to `testRunStore` so each test can set its own router before constructing.
vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} }));
vi.mock("~/v3/runStore.server", () => ({
get runStore() {
return testRunStore;
},
}));

vi.setConfig({ testTimeout: 60_000 });

function makeSplitRouter(prisma14: PrismaClient, prisma17: RunOpsPrismaClient) {
const legacyStore = new PostgresRunStore({
prisma: prisma14,
readOnlyPrisma: prisma14,
schemaVariant: "legacy",
});
const newStore = new PostgresRunStore({
prisma: prisma17 as never,
readOnlyPrisma: prisma17 as never,
schemaVariant: "dedicated",
});
return new RoutingRunStore({ new: newStore, legacy: legacyStore });
}

function authEnv(environmentId: string): AuthenticatedEnvironment {
return {
id: environmentId,
type: "DEVELOPMENT",
project: { id: "proj_split" } as Project,
organization: { id: "org_split" } as Organization,
orgMember: null,
} as unknown as AuthenticatedEnvironment;
}

heteroRunOpsPostgresTest(
"a bare ApiBatchResultsPresenter resolves a NEW-resident (ksuid) batch under the split",
async ({ prisma14, prisma17 }) => {
testRunStore = makeSplitRouter(prisma14, prisma17);

const environmentId = "env_split_res";
// ksuid internal id → classifies to the NEW store, seeded in the NEW (prisma17) DB. The
// friendlyId probe fans out NEW→LEGACY regardless of id shape, so the NEW seed is what matters.
const batchInternalId = generateKsuidId();
const batchFriendlyId = `batch_${generateKsuidId()}`;

await prisma17.batchTaskRun.create({
data: {
id: batchInternalId,
friendlyId: batchFriendlyId,
runtimeEnvironmentId: environmentId,
},
});

// Bare construction — exactly how the results route builds it.
const presenter = new ApiBatchResultsPresenter();
const result = await presenter.call(batchFriendlyId, authEnv(environmentId));

// Before the fix this 404s (undefined) because a control-plane read misses the NEW-resident batch.
expect(result).toEqual({ id: batchFriendlyId, items: [] });
}
);

heteroRunOpsPostgresTest(
"a bare ApiBatchResultsPresenter still returns undefined for a genuinely missing batch",
async ({ prisma14, prisma17 }) => {
testRunStore = makeSplitRouter(prisma14, prisma17);

const presenter = new ApiBatchResultsPresenter();
const result = await presenter.call("batch_does_not_exist", authEnv("env_none"));

expect(result).toBeUndefined();
}
);
Loading
Loading