chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,237 @@
import { describe, expect, it } from "vitest";
import {
ControlPlaneCache,
type ResolvedAuthenticatedEnv,
type ResolvedEnv,
type ResolvedRunLockedWorker,
type ResolvedWorkerVersion,
} from "./controlPlaneCache.server";
// Minimal, structurally-irrelevant stand-ins: the cache stores and returns opaque values by
// reference, so these only need to be distinguishable objects — the slot types are exercised for
// key routing, not field shape.
const anEnv = { id: "env_1", organizationId: "org_1" } as unknown as ResolvedEnv;
const aVersion = { worker: { id: "bw_1" } } as unknown as ResolvedWorkerVersion;
const anAuthEnv = {
id: "env_1",
slug: "prod",
organizationId: "org_1",
} as unknown as ResolvedAuthenticatedEnv;
const aLockedWorker = { lockedBy: null, lockedToVersion: null } as ResolvedRunLockedWorker;
describe("ControlPlaneCache", () => {
it("round-trips a value through every slot", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
cache.setEnv("env_1", anEnv);
cache.setWorkerVersion("env_1:current", aVersion);
cache.setEnvExists("env_1", true);
cache.setAuthEnv("env_1", anAuthEnv);
cache.setLockedWorker("bw_1:v_1", aLockedWorker);
expect(cache.getEnv("env_1")).toBe(anEnv);
expect(cache.getWorkerVersion("env_1:current")).toBe(aVersion);
expect(cache.getEnvExists("env_1")).toBe(true);
expect(cache.getAuthEnv("env_1")).toBe(anAuthEnv);
expect(cache.getLockedWorker("bw_1:v_1")).toBe(aLockedWorker);
});
it("returns undefined for a key that was never set, in every slot", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
expect(cache.getEnv("missing")).toBeUndefined();
expect(cache.getWorkerVersion("missing")).toBeUndefined();
expect(cache.getEnvExists("missing")).toBeUndefined();
expect(cache.getAuthEnv("missing")).toBeUndefined();
expect(cache.getLockedWorker("missing")).toBeUndefined();
});
it("distinguishes a cached null (confirmed absence) from an unset miss", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
expect(cache.getEnv("env_2")).toBeUndefined();
cache.setEnv("env_2", null);
expect(cache.getEnv("env_2")).toBeNull();
expect(cache.getAuthEnv("env_2")).toBeUndefined();
cache.setAuthEnv("env_2", null);
expect(cache.getAuthEnv("env_2")).toBeNull();
expect(cache.getWorkerVersion("env_2:current")).toBeUndefined();
cache.setWorkerVersion("env_2:current", null);
expect(cache.getWorkerVersion("env_2:current")).toBeNull();
expect(cache.getLockedWorker("_:_")).toBeUndefined();
cache.setLockedWorker("_:_", null);
expect(cache.getLockedWorker("_:_")).toBeNull();
});
it("caches a false env-existence result distinctly from an unset miss", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
expect(cache.getEnvExists("env_3")).toBeUndefined();
cache.setEnvExists("env_3", false);
expect(cache.getEnvExists("env_3")).toBe(false);
});
it("invalidateEnv forces the next getEnv to miss", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
cache.setEnv("env_4", anEnv);
expect(cache.getEnv("env_4")).toBe(anEnv);
cache.invalidateEnv("env_4");
expect(cache.getEnv("env_4")).toBeUndefined();
});
it("makes a re-setEnv after invalidation readable again", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
const replacement = { id: "env_5b" } as unknown as ResolvedEnv;
cache.setEnv("env_5", anEnv);
cache.invalidateEnv("env_5");
expect(cache.getEnv("env_5")).toBeUndefined();
cache.setEnv("env_5", replacement);
expect(cache.getEnv("env_5")).toBe(replacement);
});
it("invalidateEnv is scoped to its own id", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
const other = { id: "env_keep" } as unknown as ResolvedEnv;
cache.setEnv("env_drop", anEnv);
cache.setEnv("env_keep", other);
cache.invalidateEnv("env_drop");
expect(cache.getEnv("env_drop")).toBeUndefined();
expect(cache.getEnv("env_keep")).toBe(other);
});
it("does not collide keys across slots for the same id", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
cache.setEnv("x", anEnv);
cache.setEnvExists("x", true);
cache.setAuthEnv("x", anAuthEnv);
expect(cache.getEnv("x")).toBe(anEnv);
expect(cache.getEnvExists("x")).toBe(true);
expect(cache.getAuthEnv("x")).toBe(anAuthEnv);
// Invalidating the env slot leaves the sibling slots for the same id intact.
cache.invalidateEnv("x");
expect(cache.getEnv("x")).toBeUndefined();
expect(cache.getEnvExists("x")).toBe(true);
expect(cache.getAuthEnv("x")).toBe(anAuthEnv);
});
it("evicts the oldest entry once maxEntries is exceeded", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 2 });
cache.setEnv("first", { id: "first" } as unknown as ResolvedEnv);
cache.setEnv("second", { id: "second" } as unknown as ResolvedEnv);
cache.setEnv("third", { id: "third" } as unknown as ResolvedEnv);
expect(cache.getEnv("first")).toBeUndefined();
expect(cache.getEnv("second")).toMatchObject({ id: "second" });
expect(cache.getEnv("third")).toMatchObject({ id: "third" });
});
it("treats a zero-TTL entry as immediately expired", () => {
const cache = new ControlPlaneCache({ ttlMs: 0, maxEntries: 100 });
cache.setEnv("env_ttl", anEnv);
expect(cache.getEnv("env_ttl")).toBeUndefined();
});
it("invalidateEnvironment forces the next env/authEnv/envExists read to miss", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
cache.setEnv("env_6", anEnv);
cache.setAuthEnv("env_6", anAuthEnv);
cache.setEnvExists("env_6", true);
expect(cache.getEnv("env_6")).toBe(anEnv);
expect(cache.getAuthEnv("env_6")).toBe(anAuthEnv);
expect(cache.getEnvExists("env_6")).toBe(true);
cache.invalidateEnvironment("env_6");
expect(cache.getEnv("env_6")).toBeUndefined();
expect(cache.getAuthEnv("env_6")).toBeUndefined();
expect(cache.getEnvExists("env_6")).toBeUndefined();
});
it("invalidateEnvironment is scoped to its own id", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
const keepEnv = { id: "env_keep", organizationId: "org_1" } as unknown as ResolvedEnv;
cache.setEnv("env_drop", anEnv);
cache.setEnv("env_keep", keepEnv);
cache.invalidateEnvironment("env_drop");
expect(cache.getEnv("env_drop")).toBeUndefined();
expect(cache.getEnv("env_keep")).toBe(keepEnv);
});
it("invalidateOrganization drops env/authEnv rows for that org across every env id", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
const envA = { id: "env_a", organizationId: "org_1" } as unknown as ResolvedEnv;
const envB = { id: "env_b", organizationId: "org_1" } as unknown as ResolvedEnv;
const authA = {
id: "env_a",
slug: "a",
organizationId: "org_1",
} as unknown as ResolvedAuthenticatedEnv;
cache.setEnv("env_a", envA);
cache.setEnv("env_b", envB);
cache.setAuthEnv("env_a", authA);
expect(cache.getEnv("env_a")).toBe(envA);
expect(cache.getEnv("env_b")).toBe(envB);
expect(cache.getAuthEnv("env_a")).toBe(authA);
cache.invalidateOrganization("org_1");
// Every env/authEnv row for org_1 misses — no reverse org->env index required.
expect(cache.getEnv("env_a")).toBeUndefined();
expect(cache.getEnv("env_b")).toBeUndefined();
expect(cache.getAuthEnv("env_a")).toBeUndefined();
});
it("invalidateOrganization does not affect a different org's cached envs", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
const otherOrgEnv = { id: "env_other", organizationId: "org_2" } as unknown as ResolvedEnv;
cache.setEnv("env_1", anEnv); // org_1
cache.setEnv("env_other", otherOrgEnv); // org_2
cache.invalidateOrganization("org_1");
expect(cache.getEnv("env_1")).toBeUndefined();
expect(cache.getEnv("env_other")).toBe(otherOrgEnv);
});
it("re-setting an env after an org invalidation makes it readable again", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
cache.setEnv("env_1", anEnv);
cache.invalidateOrganization("org_1");
expect(cache.getEnv("env_1")).toBeUndefined();
// A write after the bump stamps the new org epoch, so it reads back.
cache.setEnv("env_1", anEnv);
expect(cache.getEnv("env_1")).toBe(anEnv);
});
it("a cached null env survives an org invalidation (a confirmed absence carries no org)", () => {
const cache = new ControlPlaneCache({ ttlMs: 60_000, maxEntries: 100 });
cache.setEnv("env_absent", null);
expect(cache.getEnv("env_absent")).toBeNull();
cache.invalidateOrganization("org_1");
expect(cache.getEnv("env_absent")).toBeNull();
});
});
@@ -0,0 +1,244 @@
import type {
BackgroundWorker,
BackgroundWorkerTask,
Prisma,
RuntimeEnvironmentType,
TaskQueue,
WorkerDeployment,
} from "@trigger.dev/database";
import { BoundedTtlCache } from "~/services/realtime/boundedTtlCache";
import type { AuthenticatedEnvironment } from "@trigger.dev/core/v3/auth/environment";
/**
* Cache policy + invalidation for the cross-DB control-plane resolver.
*
* One-way dependency: this module is imported by `controlPlaneResolver.server.ts`;
* it must NEVER import the resolver. The shared `Resolved*` return types live here
* so both files reference an identical definition (the resolver re-exports them for
* consumers).
*
* Invalidation note: the underlying `BoundedTtlCache` exposes no public `delete`, so
* explicit invalidation is implemented with a per-key epoch map. A write stamps the
* stored value with the key's current epoch; a read returns the value only if its
* stamped epoch still matches the current epoch, otherwise it is treated as a miss.
* `invalidate*` bumps the key's epoch, forcing the next read to miss. (If a future
* rebase gives `BoundedTtlCache` a public `delete`, prefer it and drop the epoch map.)
*
* Two invalidation scopes: `invalidateEnvironment(id)` bumps every env-keyed slot for one
* env; `invalidateOrganization(orgId)` bumps a per-org epoch that env/authEnv values are
* also stamped with at write time (no reverse org->env index needed), so all of that org's
* cached env/authEnv rows miss on the next read.
*/
export const DEFAULT_CP_CACHE_TTL_MS = 30_000;
export const DEFAULT_CP_CACHE_MAX_ENTRIES = 10_000;
export type ResolvedEnv = {
id: string;
type: RuntimeEnvironmentType;
projectId: string;
organizationId: string;
archivedAt: Date | null;
// The parent env's type, or null when this env has no parent. Alerts compute
// `parentEnvironmentType ?? type` (byte-identical to `parentEnvironment?.type ?? type`).
parentEnvironmentType: RuntimeEnvironmentType | null;
// Concurrency + nested ids the run-engine ControlPlaneResolver adapter maps to
// `ResolvedEngineEnv` (a MinimalAuthenticatedEnvironment superset). Existing app consumers
// ignore these additive fields.
maximumConcurrencyLimit: number;
concurrencyLimitBurstFactor: Prisma.Decimal;
};
/** Mirrors `WorkerDeploymentWithWorkerTasks` in `dequeueSystem.ts` exactly. */
export type ResolvedWorkerVersion = {
worker: BackgroundWorker;
tasks: BackgroundWorkerTask[];
queues: TaskQueue[];
deployment: WorkerDeployment | null;
};
// The canonical authenticated-environment shape (slug/type/project/organization/orgMember/…)
// PLUS the `git` JSON column the run-engine runAttemptSystem reads. `AuthenticatedEnvironment`
// does not carry `git`, so the intersection adds it; this matches the run-engine
// `ResolvedAuthenticatedEnv` so the engine adapter can delegate to this cached slot.
export type ResolvedAuthenticatedEnv = AuthenticatedEnvironment & { git: Prisma.JsonValue | null };
/**
* The slim `lockedBy` (BackgroundWorkerTask) + `lockedToVersion` (BackgroundWorker, with its
* WorkerDeployment) shape — the UNION of every field webapp run sites read off these two
* cross-DB worker relations. Each field is optional because a run may be locked to a version
* but not a task (or neither); resolvers return only what exists.
*/
export type ResolvedRunLockedWorker = {
lockedBy: {
id: string;
filePath: string;
exportName: string | null;
slug: string;
machineConfig: Prisma.JsonValue | null;
worker: {
id: string;
version: string;
sdkVersion: string;
cliVersion: string;
supportsLazyAttempts: boolean;
deployment: {
friendlyId: string;
shortCode: string;
version: string;
runtime: string | null;
runtimeVersion: string | null;
git: Prisma.JsonValue | null;
} | null;
};
} | null;
lockedToVersion: {
version: string;
sdkVersion: string;
runtime: string | null;
runtimeVersion: string | null;
supportsLazyAttempts: boolean;
} | null;
};
// `orgEpoch` is stamped only on slots that embed org config (env/authEnv); undefined slots
// are exempt from the org-epoch check.
type Stamped<V> = { value: V; epoch: number; orgEpoch?: number };
export class ControlPlaneCache {
readonly #env: BoundedTtlCache<Stamped<ResolvedEnv | null>>;
readonly #version: BoundedTtlCache<Stamped<ResolvedWorkerVersion | null>>;
readonly #envExists: BoundedTtlCache<Stamped<boolean>>;
readonly #authEnv: BoundedTtlCache<Stamped<ResolvedAuthenticatedEnv | null>>;
readonly #lockedWorker: BoundedTtlCache<Stamped<ResolvedRunLockedWorker | null>>;
// Explicit invalidation: bumping a key's (or org's) epoch forces the next read to miss.
readonly #epochs = new Map<string, number>();
readonly #orgEpochs = new Map<string, number>();
constructor(opts?: { ttlMs?: number; maxEntries?: number }) {
const ttl = opts?.ttlMs ?? DEFAULT_CP_CACHE_TTL_MS;
const max = opts?.maxEntries ?? DEFAULT_CP_CACHE_MAX_ENTRIES;
this.#env = new BoundedTtlCache(ttl, max);
this.#version = new BoundedTtlCache(ttl, max);
this.#envExists = new BoundedTtlCache(ttl, max);
this.#authEnv = new BoundedTtlCache(ttl, max);
this.#lockedWorker = new BoundedTtlCache(ttl, max);
}
#epoch(key: string): number {
return this.#epochs.get(key) ?? 0;
}
#orgEpoch(orgId: string): number {
return this.#orgEpochs.get(orgId) ?? 0;
}
#read<V>(cache: BoundedTtlCache<Stamped<V>>, key: string, orgId?: string): V | undefined {
const entry = cache.get(key);
if (entry === undefined || entry.epoch !== this.#epoch(key)) {
return undefined;
}
if (orgId !== undefined && entry.orgEpoch !== this.#orgEpoch(orgId)) {
return undefined;
}
return entry.value;
}
#write<V>(cache: BoundedTtlCache<Stamped<V>>, key: string, value: V, orgId?: string): void {
cache.set(key, {
value,
epoch: this.#epoch(key),
orgEpoch: orgId !== undefined ? this.#orgEpoch(orgId) : undefined,
});
}
#bump(key: string): void {
this.#epochs.set(key, this.#epoch(key) + 1);
}
getEnv(id: string): (ResolvedEnv | null) | undefined {
const entry = this.#env.get(`env:${id}`);
if (entry === undefined || entry.epoch !== this.#epoch(`env:${id}`)) {
return undefined;
}
// A cached null (or an entry written without an org) carries no org, so it can never be
// stale against an org write.
if (
entry.value !== null &&
entry.value.organizationId &&
entry.orgEpoch !== this.#orgEpoch(entry.value.organizationId)
) {
return undefined;
}
return entry.value;
}
setEnv(id: string, value: ResolvedEnv | null): void {
this.#write(this.#env, `env:${id}`, value, value?.organizationId);
}
invalidateEnv(id: string): void {
this.#bump(`env:${id}`);
}
// worker version: key = `${environmentId}:${backgroundWorkerId ?? "current"}`
getWorkerVersion(key: string): (ResolvedWorkerVersion | null) | undefined {
return this.#read(this.#version, `version:${key}`);
}
setWorkerVersion(key: string, value: ResolvedWorkerVersion | null): void {
this.#write(this.#version, `version:${key}`, value);
}
// env existence (boolean; for the dropped-FK replacement check)
getEnvExists(id: string): boolean | undefined {
return this.#read(this.#envExists, `envExists:${id}`);
}
setEnvExists(id: string, exists: boolean): void {
this.#write(this.#envExists, `envExists:${id}`, exists);
}
// full authenticated environment (toAuthenticated shape)
getAuthEnv(id: string): (ResolvedAuthenticatedEnv | null) | undefined {
const entry = this.#authEnv.get(`authEnv:${id}`);
if (entry === undefined || entry.epoch !== this.#epoch(`authEnv:${id}`)) {
return undefined;
}
if (
entry.value !== null &&
entry.value.organizationId &&
entry.orgEpoch !== this.#orgEpoch(entry.value.organizationId)
) {
return undefined;
}
return entry.value;
}
setAuthEnv(id: string, value: ResolvedAuthenticatedEnv | null): void {
this.#write(this.#authEnv, `authEnv:${id}`, value, value?.organizationId);
}
/**
* Invalidate every env-keyed slot for a single environment. Call this from a control-plane
* write that mutates one env's config (pause/resume, archive, concurrency/burst-factor).
*/
invalidateEnvironment(id: string): void {
this.#bump(`env:${id}`);
this.#bump(`authEnv:${id}`);
this.#bump(`envExists:${id}`);
}
/**
* Invalidate every cached env/authEnv row belonging to an organization. Call this from a
* control-plane write that mutates org-level config (feature flags, org concurrency, runs
* enable/disable, rate limits) — it affects the org object embedded in each of the org's envs.
*/
invalidateOrganization(orgId: string): void {
this.#orgEpochs.set(orgId, this.#orgEpoch(orgId) + 1);
}
// run-locked worker (lockedBy + lockedToVersion); key = `${lockedById ?? "_"}:${lockedToVersionId ?? "_"}`
getLockedWorker(key: string): (ResolvedRunLockedWorker | null) | undefined {
return this.#read(this.#lockedWorker, `lockedWorker:${key}`);
}
setLockedWorker(key: string, value: ResolvedRunLockedWorker | null): void {
this.#write(this.#lockedWorker, `lockedWorker:${key}`, value);
}
}
@@ -0,0 +1,463 @@
import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic";
import type {
PrismaClient,
PrismaReplicaClient,
RuntimeEnvironmentType,
} from "@trigger.dev/database";
import { prisma, $replica } from "~/db.server";
import { env } from "~/env.server";
import {
ControlPlaneCache,
DEFAULT_CP_CACHE_MAX_ENTRIES,
DEFAULT_CP_CACHE_TTL_MS,
type ResolvedAuthenticatedEnv,
type ResolvedEnv,
type ResolvedWorkerVersion,
type ResolvedRunLockedWorker,
} from "./controlPlaneCache.server";
import { authIncludeWithParent, toAuthenticated } from "~/models/runtimeEnvironment.server";
/**
* App-level control-plane resolution + cache layer. Replaces the run-ops -> control-plane
* Prisma joins (env/project/org, the pinned/current worker version + its tasks/queues, the
* TaskQueue, the TaskSchedule friendlyId mapping) with cached lookups against the
* control-plane client, so the split (cross-DB) hot path avoids a cross-WAN round-trip per
* resolution.
*
* Split ON (cloud): cache-first reads against the control-plane replica; `null` is cached as
* a confirmed absence. Split OFF (self-host/local/CI): plain Prisma join against the single
* control-plane client on every call, NO cache — byte-identical to today's inline join.
*
* The split gate is a SYNCHRONOUS `splitEnabled: () => boolean` injected at construction; the
* resolver never awaits the async `isSplitEnabled()` (that gate is reserved for the boot
* sentinel). Tests inject testcontainer clients + a sync predicate; only the module-level
* singleton at the bottom reads from `db.server.ts` / `env.server.ts`.
*
* Scope boundary: this unit owns ONLY control-plane resolution (env, worker version,
* env existence). The run-ops batchId friendlyId->id resolution belongs to the
* run-ops read path (the unit owning `runsRepository.server.ts`); do not duplicate it here.
*/
export { ResolvedEnv, ResolvedWorkerVersion };
export type { ResolvedAuthenticatedEnv, ResolvedRunLockedWorker };
/** Thrown by `assertEnvExists` when a referenced control-plane env does not exist. */
export class ControlPlaneReferenceError extends Error {
constructor(message: string) {
super(message);
this.name = "ControlPlaneReferenceError";
}
}
export type ControlPlaneResolverOptions = {
controlPlanePrimary: PrismaClient;
controlPlaneReplica: PrismaReplicaClient;
cache: ControlPlaneCache;
splitEnabled: () => boolean;
};
type CpClient = PrismaClient | PrismaReplicaClient;
function workerVersionKey(
environmentId: string,
backgroundWorkerId: string | undefined,
type: RuntimeEnvironmentType | undefined
): string {
return `${environmentId}:${backgroundWorkerId ?? "current"}:${type ?? "any"}`;
}
function lockedWorkerKey(lockedById?: string | null, lockedToVersionId?: string | null): string {
return `${lockedById ?? "_"}:${lockedToVersionId ?? "_"}`;
}
export class ControlPlaneResolver {
private readonly controlPlanePrimary: PrismaClient;
private readonly controlPlaneReplica: PrismaReplicaClient;
private readonly cache: ControlPlaneCache;
private readonly splitEnabled: () => boolean;
constructor(opts: ControlPlaneResolverOptions) {
this.controlPlanePrimary = opts.controlPlanePrimary;
this.controlPlaneReplica = opts.controlPlaneReplica;
this.cache = opts.cache;
this.splitEnabled = opts.splitEnabled;
}
async resolveEnv(environmentId: string): Promise<ResolvedEnv | null> {
if (!this.splitEnabled()) {
return this.#queryEnv(this.controlPlanePrimary, environmentId);
}
const cached = this.cache.getEnv(environmentId);
if (cached !== undefined) {
return cached;
}
const resolved = await this.#queryEnv(this.controlPlaneReplica, environmentId);
this.cache.setEnv(environmentId, resolved);
return resolved;
}
async #queryEnv(client: CpClient, environmentId: string): Promise<ResolvedEnv | null> {
const env = await client.runtimeEnvironment.findFirst({
where: { id: environmentId },
select: {
id: true,
type: true,
projectId: true,
archivedAt: true,
maximumConcurrencyLimit: true,
concurrencyLimitBurstFactor: true,
project: { select: { organizationId: true } },
parentEnvironment: { select: { type: true } },
},
});
if (!env) {
return null;
}
return {
id: env.id,
type: env.type,
projectId: env.projectId,
organizationId: env.project.organizationId,
archivedAt: env.archivedAt,
parentEnvironmentType: env.parentEnvironment?.type ?? null,
maximumConcurrencyLimit: env.maximumConcurrencyLimit,
concurrencyLimitBurstFactor: env.concurrencyLimitBurstFactor,
};
}
async resolveAuthenticatedEnv(environmentId: string): Promise<ResolvedAuthenticatedEnv | null> {
if (!this.splitEnabled()) {
return this.#queryAuthenticatedEnv(this.controlPlanePrimary, environmentId);
}
const cached = this.cache.getAuthEnv(environmentId);
if (cached !== undefined) {
return cached;
}
const resolved = await this.#queryAuthenticatedEnv(this.controlPlaneReplica, environmentId);
this.cache.setAuthEnv(environmentId, resolved);
return resolved;
}
async #queryAuthenticatedEnv(
client: CpClient,
environmentId: string
): Promise<ResolvedAuthenticatedEnv | null> {
const env = await client.runtimeEnvironment.findFirst({
where: { id: environmentId },
include: authIncludeWithParent,
});
if (!env) {
return null;
}
// `authIncludeWithParent` returns all RuntimeEnvironment scalars on the row (including
// `git`), so we map the auth shape via toAuthenticated() and add `git` from the same row.
return { ...toAuthenticated(env), git: env.git };
}
async resolveRunLockedWorker(args: {
lockedById?: string | null;
lockedToVersionId?: string | null;
}): Promise<ResolvedRunLockedWorker | null> {
const { lockedById, lockedToVersionId } = args;
if (!this.splitEnabled()) {
return this.#queryRunLockedWorker(this.controlPlanePrimary, lockedById, lockedToVersionId);
}
const key = lockedWorkerKey(lockedById, lockedToVersionId);
const cached = this.cache.getLockedWorker(key);
if (cached !== undefined) {
return cached;
}
const resolved = await this.#queryRunLockedWorker(
this.controlPlaneReplica,
lockedById,
lockedToVersionId
);
this.cache.setLockedWorker(key, resolved);
return resolved;
}
async #queryRunLockedWorker(
client: CpClient,
lockedById?: string | null,
lockedToVersionId?: string | null
): Promise<ResolvedRunLockedWorker | null> {
const lockedByRow = lockedById
? await client.backgroundWorkerTask.findFirst({
where: { id: lockedById },
select: {
id: true,
filePath: true,
exportName: true,
slug: true,
machineConfig: true,
worker: {
select: {
id: true,
version: true,
sdkVersion: true,
cliVersion: true,
supportsLazyAttempts: true,
deployment: {
select: {
friendlyId: true,
shortCode: true,
version: true,
runtime: true,
runtimeVersion: true,
git: true,
},
},
},
},
},
})
: null;
const lockedToVersionRow = lockedToVersionId
? await client.backgroundWorker.findFirst({
where: { id: lockedToVersionId },
select: {
version: true,
sdkVersion: true,
runtime: true,
runtimeVersion: true,
supportsLazyAttempts: true,
},
})
: null;
return {
lockedBy: lockedByRow,
lockedToVersion: lockedToVersionRow,
};
}
async resolveWorkerVersion(args: {
environmentId: string;
backgroundWorkerId?: string;
/**
* When provided, the full run-engine dequeue dispatch is used (DEV resolves the most-recent
* worker; deployed resolves the promoted MANAGED deployment with the latest-v2 fallback).
* When omitted, the original app behavior applies (worker-by-id, else current promotion).
*/
type?: RuntimeEnvironmentType;
}): Promise<ResolvedWorkerVersion | null> {
const { environmentId, backgroundWorkerId, type } = args;
if (!this.splitEnabled()) {
return this.#queryWorkerVersion(
this.controlPlanePrimary,
environmentId,
backgroundWorkerId,
type
);
}
const key = workerVersionKey(environmentId, backgroundWorkerId, type);
const cached = this.cache.getWorkerVersion(key);
if (cached !== undefined) {
return cached;
}
const resolved = await this.#queryWorkerVersion(
this.controlPlaneReplica,
environmentId,
backgroundWorkerId,
type
);
this.cache.setWorkerVersion(key, resolved);
return resolved;
}
async #queryWorkerVersion(
client: CpClient,
environmentId: string,
backgroundWorkerId?: string,
type?: RuntimeEnvironmentType
): Promise<ResolvedWorkerVersion | null> {
// Full run-engine dequeue dispatch (mirrors dequeueSystem's four helpers) when the env type is
// known. DEVELOPMENT envs resolve by most-recent worker; deployed envs resolve the promoted
// MANAGED deployment.
if (type === "DEVELOPMENT") {
return backgroundWorkerId
? this.#queryWorkerById(client, backgroundWorkerId)
: this.#queryMostRecentWorker(client, environmentId);
}
if (backgroundWorkerId) {
const worker = await client.backgroundWorker.findFirst({
where: { id: backgroundWorkerId },
include: { deployment: true, tasks: true, queues: true },
});
if (!worker) {
return null;
}
return {
worker,
tasks: worker.tasks,
queues: worker.queues,
deployment: worker.deployment,
};
}
// Deployed env, no workerId: resolve the currently-promoted deployment's worker. When `type`
// is known (engine dispatch) apply the MANAGED guard + latest-v2 fallback that the run-engine
// path requires; without `type` keep the original app behavior (return the promoted worker).
const promotion = await client.workerDeploymentPromotion.findFirst({
where: { environmentId, label: CURRENT_DEPLOYMENT_LABEL },
include: {
deployment: {
include: { worker: { include: { tasks: true, queues: true } } },
},
},
});
if (!promotion?.deployment.worker) {
return null;
}
if (type === undefined || promotion.deployment.type === "MANAGED") {
return {
worker: promotion.deployment.worker,
tasks: promotion.deployment.worker.tasks,
queues: promotion.deployment.worker.queues,
deployment: promotion.deployment,
};
}
// Engine dispatch only: the promoted deployment is not run-engine v2; fall back to the latest
// MANAGED deployment.
const latestV2Deployment = await client.workerDeployment.findFirst({
where: { environmentId, type: "MANAGED" },
orderBy: { id: "desc" },
include: { worker: { include: { tasks: true, queues: true } } },
});
if (!latestV2Deployment?.worker) {
return null;
}
return {
worker: latestV2Deployment.worker,
tasks: latestV2Deployment.worker.tasks,
queues: latestV2Deployment.worker.queues,
deployment: latestV2Deployment,
};
}
async #queryWorkerById(
client: CpClient,
workerId: string
): Promise<ResolvedWorkerVersion | null> {
const worker = await client.backgroundWorker.findFirst({
where: { id: workerId },
include: { deployment: true, tasks: true, queues: true },
orderBy: { id: "desc" },
});
if (!worker) {
return null;
}
return { worker, tasks: worker.tasks, queues: worker.queues, deployment: worker.deployment };
}
async #queryMostRecentWorker(
client: CpClient,
environmentId: string
): Promise<ResolvedWorkerVersion | null> {
const worker = await client.backgroundWorker.findFirst({
where: { runtimeEnvironmentId: environmentId },
include: { tasks: true, queues: true },
orderBy: { id: "desc" },
});
if (!worker) {
return null;
}
return { worker, tasks: worker.tasks, queues: worker.queues, deployment: null };
}
async assertEnvExists(environmentId: string): Promise<void> {
if (!this.splitEnabled()) {
// Split OFF = single DB, so run and env are co-located and there is no FK/check
// to replace (matches main). Skip the hot-path read entirely.
return;
}
const cached = this.cache.getEnvExists(environmentId);
if (cached !== undefined) {
if (!cached) {
throw new ControlPlaneReferenceError(
`Referenced environment does not exist: ${environmentId}`
);
}
return;
}
const exists = await this.#queryEnvExists(this.controlPlaneReplica, environmentId);
this.cache.setEnvExists(environmentId, exists);
if (!exists) {
throw new ControlPlaneReferenceError(
`Referenced environment does not exist: ${environmentId}`
);
}
}
async #queryEnvExists(client: CpClient, environmentId: string): Promise<boolean> {
const env = await client.runtimeEnvironment.findFirst({
where: { id: environmentId },
select: { id: true },
});
return env !== null;
}
/**
* Drop cached control-plane rows for one environment after a control-plane write to that
* env's config. A no-op when split is OFF (nothing is cached), so it is always safe to call.
*/
invalidateEnvironment(environmentId: string): void {
this.cache.invalidateEnvironment(environmentId);
}
/**
* Drop cached env/authEnv rows for every environment of an organization after a
* control-plane write to that org's config. Safe under split OFF (no cache).
*/
invalidateOrganization(organizationId: string): void {
this.cache.invalidateOrganization(organizationId);
}
}
// Module-level singleton: wires the real control-plane clients + env split predicate.
// The control-plane writer/replica are the unchanged `prisma` / `$replica` exports. The
// split decision is a boot constant derived once from the env predicate (same one the
// run-ops topology factory uses); the async isSplitEnabled() distinct-DB sentinel is enforced
// at boot elsewhere and is never awaited on a resolver hot path.
const SPLIT_ENABLED =
env.RUN_OPS_SPLIT_ENABLED && !!env.RUN_OPS_DATABASE_URL && !!env.RUN_OPS_LEGACY_DATABASE_URL;
export const controlPlaneResolver = new ControlPlaneResolver({
controlPlanePrimary: prisma,
controlPlaneReplica: $replica,
// Relax the cache via config. Unset env knobs -> built-in defaults (byte-identical).
cache: new ControlPlaneCache({
ttlMs: env.CONTROL_PLANE_CACHE_TTL_MS ?? DEFAULT_CP_CACHE_TTL_MS,
maxEntries: env.CONTROL_PLANE_CACHE_MAX_ENTRIES ?? DEFAULT_CP_CACHE_MAX_ENTRIES,
}),
splitEnabled: () => SPLIT_ENABLED,
});
@@ -0,0 +1,93 @@
import { ownerEngine } from "@trigger.dev/core/v3/isomorphic";
import { isSplitEnabled } from "./splitMode.server";
import type {
CrossSeamGuardDecision,
CrossSeamGuardInput,
RunOpsResidency,
StoreTarget,
UnblockRouteKind,
} from "./types";
const KNOWN_ROUTE_KINDS: ReadonlySet<UnblockRouteKind> = new Set<UnblockRouteKind>([
"MANUAL",
"DATETIME",
"RESUME_TOKEN",
"IDEMPOTENCY_REUSE",
"RUN",
]);
// There is NO default store: an unrecognised route is a loud failure.
function assertKnownRouteKind(routeKind: UnblockRouteKind): void {
if (!KNOWN_ROUTE_KINDS.has(routeKind)) {
throw new Error(`Unknown unblock routeKind: ${JSON.stringify(routeKind)}`);
}
}
function storeForResidency(residency: RunOpsResidency): StoreTarget {
return residency === "NEW" ? "new" : "legacy";
}
/**
* Pin precedence (deterministic, documented order):
* 1. non-tree-owned (treeOwnerResidency === "LEGACY")
* 2. cross-tree-idempotency (isCrossTreeIdempotency === true)
* 3. legacy-parent-descendant (hasLegacyParent === true)
* Any hit overrides the store to "legacy"; the waitpoint's own residency is
* preserved on the decision so callers/metrics can see "NEW pinned to legacy".
*/
function applyPinningRules(
input: CrossSeamGuardInput
): CrossSeamGuardDecision["pinnedReason"] | undefined {
if (input.treeOwnerResidency === "LEGACY") return "non-tree-owned";
if (input.isCrossTreeIdempotency === true) return "cross-tree-idempotency";
if (input.hasLegacyParent === true) return "legacy-parent-descendant";
return undefined;
}
/**
* Pure store-selection core. No env import, no I/O — driven exhaustively by the
* downstream proof harness via the optional `classify` seam.
*/
export function selectStoreForWaitpoint(
input: CrossSeamGuardInput,
deps?: { classify?: (id: string) => RunOpsResidency }
): CrossSeamGuardDecision {
assertKnownRouteKind(input.routeKind);
const classify = deps?.classify ?? ownerEngine;
const residency: RunOpsResidency = classify(input.waitpointId);
const pinnedReason = applyPinningRules(input);
const store: StoreTarget = pinnedReason ? "legacy" : storeForResidency(residency);
return {
store,
residency,
routeKind: input.routeKind,
...(pinnedReason ? { pinnedReason } : {}),
};
}
/**
* Pure flag-aware core. In single-DB mode "legacy" IS the single store, so we
* return it WITHOUT ever consulting the classifier (off in single-DB). When
* split is on, delegate to the pure selection core.
*/
export function computeStoreForCompletion(
input: CrossSeamGuardInput,
opts: { splitEnabled: boolean; classify?: (id: string) => RunOpsResidency }
): CrossSeamGuardDecision {
if (opts.splitEnabled === false) {
return { store: "legacy", residency: "LEGACY", routeKind: input.routeKind };
}
return selectStoreForWaitpoint(input, { classify: opts.classify });
}
/** Thin server entry the waitpoint-completion consumers call. */
export async function pickRunOpsStoreForCompletion(
input: CrossSeamGuardInput
): Promise<CrossSeamGuardDecision> {
const splitEnabled = await isSplitEnabled();
return computeStoreForCompletion(input, { splitEnabled });
}
@@ -0,0 +1,55 @@
import { PrismaClient } from "@trigger.dev/database";
type DatabaseFingerprint = { systemIdentifier: string; databaseName: string };
async function readDatabaseFingerprint(url: string): Promise<DatabaseFingerprint> {
const client = new PrismaClient({ datasources: { db: { url } } });
try {
const rows = await client.$queryRawUnsafe<
Array<{ system_identifier: string; database_name: string }>
>(
"SELECT system_identifier::text AS system_identifier, current_database() AS database_name FROM pg_control_system()"
);
const row = rows[0];
if (!row) {
throw new Error("distinct-db sentinel: pg_control_system() returned no rows");
}
return { systemIdentifier: row.system_identifier, databaseName: row.database_name };
} finally {
await client.$disconnect();
}
}
export async function probeDistinctDatabases(
legacyUrl: string,
newUrl: string,
opts?: { logger?: { warn: (msg: string, meta?: Record<string, unknown>) => void } }
): Promise<{ distinct: true } | { distinct: false; reason: string }> {
try {
const [legacy, next] = await Promise.all([
readDatabaseFingerprint(legacyUrl),
readDatabaseFingerprint(newUrl),
]);
const sameCluster = legacy.systemIdentifier === next.systemIdentifier;
const sameDb = sameCluster && legacy.databaseName === next.databaseName;
// Same-cluster-different-database policy: two databases inside the SAME cluster
// (same system identifier, different current_database()) are reported distinct: true.
// That is acceptable — they are genuinely separate Postgres databases with separate
// WAL-visible state for our purposes, and the Cloud topology always uses separate
// clusters anyway. A stricter "must be a different cluster" policy would gate on
// sameCluster alone; that is flagged as an open question, not decided here.
if (sameDb) {
const reason =
"run-ops legacy and new URLs resolve to the SAME physical database " +
`(systemIdentifier=${legacy.systemIdentifier}, database=${legacy.databaseName}); ` +
"refusing to enable split — pooler/replica likely.";
opts?.logger?.warn(reason);
return { distinct: false, reason };
}
return { distinct: true };
} catch (error) {
const reason = `distinct-db sentinel probe failed; failing closed (single-DB). ${String(error)}`;
opts?.logger?.warn(reason, { error });
return { distinct: false, reason };
}
}
@@ -0,0 +1,31 @@
import {
BatchId,
classifyKind,
generateRunOpsId,
parseRunId,
REGION_CODES,
} from "@trigger.dev/core/v3/isomorphic";
import { describe, expect, it } from "vitest";
import { mintAnchoredRunFriendlyId } from "./mintAnchoredRunFriendlyId.server";
describe("mintAnchoredRunFriendlyId", () => {
it("a run-ops (NEW) batch anchor yields a run-ops (NEW) item friendlyId", () => {
const batchFriendlyId = BatchId.toFriendlyId(generateRunOpsId());
const itemFriendlyId = mintAnchoredRunFriendlyId(batchFriendlyId);
expect(classifyKind(itemFriendlyId)).toBe("runOpsId");
});
it("a cuid (LEGACY) batch anchor yields a cuid (LEGACY) item friendlyId", () => {
const batchFriendlyId = BatchId.generate().friendlyId;
const itemFriendlyId = mintAnchoredRunFriendlyId(batchFriendlyId);
expect(classifyKind(itemFriendlyId)).toBe("cuid");
});
it("stamps the requested region char into a run-ops id", () => {
const batchFriendlyId = BatchId.toFriendlyId(generateRunOpsId());
const itemFriendlyId = mintAnchoredRunFriendlyId(batchFriendlyId, "us-east-1");
const parsed = parseRunId(itemFriendlyId);
expect(parsed.format).toBe("b32hex");
expect(parsed.format === "b32hex" && parsed.region).toBe(REGION_CODES["us-east-1"]);
});
});
@@ -0,0 +1,15 @@
import { generateRunOpsId, RunId, type ResidencyKind } from "@trigger.dev/core/v3/isomorphic";
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;
}
// 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);
}
@@ -0,0 +1,107 @@
import { describe, expect, it, vi } from "vitest";
import { batchIdForMintKind, resolveBatchMintKind } from "./mintBatchFriendlyId.server";
import { classifyKind } from "@trigger.dev/core/v3/isomorphic";
describe("batchIdForMintKind (pure)", () => {
it("'runOpsId' kind -> 26-char classifiable NEW batch id (no 21-char ids)", () => {
const r = batchIdForMintKind("runOpsId");
expect(r.friendlyId.startsWith("batch_")).toBe(true);
expect(r.id.length).toBe(26);
expect(classifyKind(r.id)).toBe("runOpsId");
expect(classifyKind(r.friendlyId)).toBe("runOpsId");
});
it("cuid -> 25-char classifiable LEGACY batch id", () => {
const r = batchIdForMintKind("cuid");
expect(r.id.length).toBe(25);
expect(classifyKind(r.id)).toBe("cuid");
expect(classifyKind(r.friendlyId)).toBe("cuid");
});
it("never mints a 21-char id", () => {
for (const kind of ["cuid", "runOpsId"] as const) {
expect([25, 26]).toContain(batchIdForMintKind(kind).id.length);
}
});
});
describe("resolveBatchMintKind", () => {
const environment = { organizationId: "org_1", id: "env_1", orgFeatureFlags: {} };
it("ROOT batch (no parent) resolves per-org kind via resolveRunIdMintKind", async () => {
const resolveRunIdMintKind = vi.fn().mockResolvedValue("runOpsId");
const kind = await resolveBatchMintKind({
environment,
deps: { resolveRunIdMintKind },
});
expect(kind).toBe("runOpsId");
expect(resolveRunIdMintKind).toHaveBeenCalledWith({
organizationId: "org_1",
id: "env_1",
orgFeatureFlags: {},
});
});
it("ROOT batch on a non-cut-over org -> cuid", async () => {
const resolveRunIdMintKind = vi.fn().mockResolvedValue("cuid");
const kind = await resolveBatchMintKind({
environment,
deps: { resolveRunIdMintKind },
});
expect(kind).toBe("cuid");
});
it("CHILD batch inherits a run-ops (NEW) parent by id-shape", async () => {
const parentRunFriendlyId = `run_${"a".repeat(24) + "01"}`;
const resolveRunIdMintKind = vi.fn();
const kind = await resolveBatchMintKind({
environment,
parentRunFriendlyId,
deps: { resolveRunIdMintKind },
});
expect(kind).toBe("runOpsId");
expect(resolveRunIdMintKind).not.toHaveBeenCalled();
});
it("CHILD batch inherits a cuid (LEGACY) parent by id-shape", async () => {
const parentRunFriendlyId = `run_${"a".repeat(25)}`;
const resolveRunIdMintKind = vi.fn();
const kind = await resolveBatchMintKind({
environment,
parentRunFriendlyId,
deps: { resolveRunIdMintKind },
});
expect(kind).toBe("cuid");
expect(resolveRunIdMintKind).not.toHaveBeenCalled();
});
// mint-on-FLIP invariant: a child follows its parent's store even after the org flag
// flips the other way. The flag resolver must NEVER be consulted for a child.
it("FLIP 'cuid'->'runOpsId': a cuid (LEGACY) parent still mints a cuid child though the flag now says 'runOpsId'", async () => {
const parentRunFriendlyId = `run_${"a".repeat(25)}`;
const resolveRunIdMintKind = vi.fn().mockResolvedValue("runOpsId"); // flag flipped to runOpsId
const kind = await resolveBatchMintKind({
environment,
parentRunFriendlyId,
deps: { resolveRunIdMintKind },
});
expect(kind).toBe("cuid");
expect(resolveRunIdMintKind).not.toHaveBeenCalled();
});
it("FLIP 'runOpsId'->'cuid': a run-ops (NEW) parent still mints a run-ops child though the flag now says 'cuid'", async () => {
const parentRunFriendlyId = `run_${"a".repeat(24) + "01"}`;
const resolveRunIdMintKind = vi.fn().mockResolvedValue("cuid"); // flag flipped back to cuid
const kind = await resolveBatchMintKind({
environment,
parentRunFriendlyId,
deps: { resolveRunIdMintKind },
});
expect(kind).toBe("runOpsId");
expect(resolveRunIdMintKind).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,45 @@
import { BatchId, generateRunOpsId } from "@trigger.dev/core/v3/isomorphic";
import {
resolveRunIdMintKind as defaultResolveRunIdMintKind,
type RunIdMintKind,
} from "~/v3/engineVersion.server";
import { resolveInheritedMintKind } from "~/v3/runOpsMigration/resolveInheritedMintKind.server";
type ResolveDeps = {
resolveRunIdMintKind: typeof defaultResolveRunIdMintKind;
};
const defaultDeps: ResolveDeps = {
resolveRunIdMintKind: defaultResolveRunIdMintKind,
};
export function batchIdForMintKind(kind: RunIdMintKind): { id: string; friendlyId: string } {
if (kind === "runOpsId") {
const id = generateRunOpsId();
return { id, friendlyId: BatchId.toFriendlyId(id) };
}
return BatchId.generate();
}
export async function resolveBatchMintKind(args: {
environment: { organizationId: string; id: string; orgFeatureFlags?: unknown };
parentRunFriendlyId?: string;
deps?: Partial<ResolveDeps>;
}): Promise<RunIdMintKind> {
const deps = { ...defaultDeps, ...args.deps };
return args.parentRunFriendlyId
? resolveInheritedMintKind(args.parentRunFriendlyId)
: deps.resolveRunIdMintKind({
organizationId: args.environment.organizationId,
id: args.environment.id,
orgFeatureFlags: args.environment.orgFeatureFlags,
});
}
export async function mintBatchFriendlyId(args: {
environment: { organizationId: string; id: string; orgFeatureFlags?: unknown };
parentRunFriendlyId?: string;
deps?: Partial<ResolveDeps>;
}): Promise<{ id: string; friendlyId: string }> {
return batchIdForMintKind(await resolveBatchMintKind(args));
}
@@ -0,0 +1,153 @@
// Real legacy-replica + new-DB proof for the read-through layer.
// We NEVER mock the DB: the reads run as real `$queryRaw` against the two containers,
// crossing the actual legacy↔new boundary the split relies on. The only injected
// fakes are the pure boundaries — `isPastRetention`, `splitEnabled` — plus throwing
// spies used to assert a store was NEVER touched.
import { heteroPostgresTest } from "@internal/testcontainers";
import { describe, expect, vi } from "vitest";
import type { PrismaReplicaClient } from "~/db.server";
import { readThroughRun, type ReadThroughResult } from "./readThrough.server";
vi.setConfig({ testTimeout: 60_000 });
// 25-char cuid body → LEGACY residency. 26-char v1 body (version "1" at index 25) → NEW residency.
const LEGACY_RUN_ID = "run_" + "a".repeat(25);
const NEW_RUN_ID = "run_" + "b".repeat(24) + "01";
// Lightweight real read: a trivial `$queryRaw` that genuinely hits the given container.
// `hit` controls whether the read "finds" the run, so we exercise routing without
// seeding a full TaskRun (many required FKs) — the routing DoD is store-order, not shape.
async function realRead(
client: PrismaReplicaClient,
hit: boolean
): Promise<{ marker: number } | null> {
const rows = await client.$queryRaw<{ marker: number }[]>`SELECT 1 AS marker`;
return hit ? (rows[0] ?? null) : null;
}
// A presenter-shaped mapping: both "not-found" and "past-retention" collapse to the
// same 404-ish surface, so an old run after termination yields the normal response.
function toHttpish<T>(result: ReadThroughResult<T>): { status: number; value?: T } {
switch (result.source) {
case "new":
case "legacy-replica":
return { status: 200, value: result.value };
case "not-found":
case "past-retention":
return { status: 404 };
}
}
describe("readThroughRun (legacy replica + new DB)", () => {
heteroPostgresTest(
"old in-retention run is served from the legacy REPLICA, never a primary",
async ({ prisma14, prisma17 }) => {
// legacy hit, new miss. The layer has NO legacy-writer handle at all — the
// read resolving through `legacyReplica` (prisma14) IS the structural guarantee
// that the primary is never touched.
const result = await readThroughRun({
runId: LEGACY_RUN_ID,
environmentId: "env_1",
readNew: (c) => realRead(c, false),
readLegacy: (c) => realRead(c, true),
deps: {
splitEnabled: true,
newClient: prisma17 as unknown as PrismaReplicaClient,
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
},
});
expect(result.source).toBe("legacy-replica");
expect(toHttpish(result).status).toBe(200);
}
);
heteroPostgresTest(
"post-termination past-retention returns the normal not-found surface",
async ({ prisma14, prisma17 }) => {
const pastRetentionResult = await readThroughRun({
runId: LEGACY_RUN_ID,
environmentId: "env_1",
readNew: (c) => realRead(c, false),
readLegacy: (c) => realRead(c, false), // legacy gone / retention elapsed
deps: {
splitEnabled: true,
newClient: prisma17 as unknown as PrismaReplicaClient,
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
isPastRetention: () => true,
},
});
expect(pastRetentionResult.source).toBe("past-retention");
// A run that is simply absent (not past retention) yields not-found.
const notFoundResult = await readThroughRun({
runId: LEGACY_RUN_ID,
environmentId: "env_1",
readNew: (c) => realRead(c, false),
readLegacy: (c) => realRead(c, false),
deps: {
splitEnabled: true,
newClient: prisma17 as unknown as PrismaReplicaClient,
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
isPastRetention: () => false,
},
});
expect(notFoundResult.source).toBe("not-found");
// Both collapse to the same 404-ish surface.
expect(toHttpish(pastRetentionResult).status).toBe(toHttpish(notFoundResult).status);
expect(toHttpish(pastRetentionResult).status).toBe(404);
}
);
heteroPostgresTest(
"single-DB passthrough — only readNew runs, legacy never touched",
async ({ prisma14, prisma17 }) => {
const throwingLegacy = vi.fn(async (): Promise<{ marker: number } | null> => {
throw new Error("readLegacy must never run in single-DB mode");
});
const newRead = vi.fn((c: PrismaReplicaClient) => realRead(c, true));
const result = await readThroughRun({
runId: LEGACY_RUN_ID,
environmentId: "env_1",
readNew: newRead,
readLegacy: throwingLegacy,
deps: {
splitEnabled: false,
newClient: prisma17 as unknown as PrismaReplicaClient,
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
},
});
expect(result.source).toBe("new");
expect(newRead).toHaveBeenCalledTimes(1);
expect(throwingLegacy).not.toHaveBeenCalled();
}
);
heteroPostgresTest(
"new-residency fast-path — legacy replica is never touched",
async ({ prisma14, prisma17 }) => {
const throwingLegacy = vi.fn(async (): Promise<{ marker: number } | null> => {
throw new Error("readLegacy must never run for a NEW-residency id");
});
const result = await readThroughRun({
runId: NEW_RUN_ID,
environmentId: "env_1",
readNew: (c) => realRead(c, true),
readLegacy: throwingLegacy,
deps: {
splitEnabled: true,
newClient: prisma17 as unknown as PrismaReplicaClient,
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
},
});
expect(result.source).toBe("new");
expect(throwingLegacy).not.toHaveBeenCalled();
}
);
});
@@ -0,0 +1,106 @@
/**
* Read-through reads the LEGACY RUN-OPS READ REPLICA ONLY — never the legacy primary
* (which carries the read load we are shedding). Disabled entirely when isSplitEnabled()
* is false (single-DB passthrough).
*
* During the retention window, old run-ops rows are served off the legacy read replica.
* Residency is decided purely by id-shape: a run-ops id (NEW) id reads new only, a cuid
* (LEGACY) id reads legacy only. An unclassifiable id falls back to a new-then-legacy
* probe. After termination, past-retention runs return the normal not-found response.
* Patterned on `mollifier/resolveRunForMutation.server.ts` (`?? default` DI), but with
* the legacy-primary/writer fallback deliberately removed: this layer has NO legacy-writer
* handle at all (structural guarantee).
*/
import type { PrismaReplicaClient } from "~/db.server";
import {
runOpsLegacyReplica as defaultLegacyReplica,
runOpsNewReplica as defaultNewClient,
} from "~/db.server";
import { logger as defaultLogger } from "~/services/logger.server";
import { ownerEngine, UnclassifiableRunId } from "@trigger.dev/core/v3/isomorphic";
import { isSplitEnabled } from "./splitMode.server";
export type ReadThroughSource = "new" | "legacy-replica";
export type ReadThroughResult<T> =
| { source: ReadThroughSource; value: T }
| { source: "not-found" }
| { source: "past-retention" };
export type ReadThroughDeps = {
newClient?: PrismaReplicaClient;
legacyReplica?: PrismaReplicaClient;
/** Resolved boot constant; never `await`ed per-request when supplied. */
splitEnabled?: boolean;
isPastRetention?: (runId: string) => boolean;
logger?: { warn: (m: string, meta?: unknown) => void };
/** Saturation-signal emit hook: called on each legacy-replica hit. */
onLegacyReplicaRead?: (runId: string) => void;
};
type ReadThroughRunInput<T> = {
runId: string;
environmentId: string;
readNew: (client: PrismaReplicaClient) => Promise<T | null>;
readLegacy: (replica: PrismaReplicaClient) => Promise<T | null>;
deps?: ReadThroughDeps;
};
export async function readThroughRun<T>(
input: ReadThroughRunInput<T>
): Promise<ReadThroughResult<T>> {
const { runId, deps } = input;
const newClient = deps?.newClient ?? defaultNewClient;
const legacyReplica = deps?.legacyReplica ?? defaultLegacyReplica;
const logger = deps?.logger ?? defaultLogger;
const splitEnabled = deps?.splitEnabled ?? (await isSplitEnabled());
// Passthrough: single plain read against the one collapsed store. No legacy read,
// no second connection.
if (!splitEnabled) {
const v = await input.readNew(newClient);
return v != null ? { source: "new", value: v } : { source: "not-found" };
}
// Split is on. Classify residency; an unclassifiable id is treated as LEGACY
// (conservative — probe rather than drop a real run).
let residency: "LEGACY" | "NEW";
try {
residency = ownerEngine(runId);
} catch (e) {
if (e instanceof UnclassifiableRunId) {
logger.warn("readThroughRun: UnclassifiableRunId, treating as LEGACY", {
runId,
valueLength: e.valueLength,
});
residency = "LEGACY";
} else {
throw e;
}
}
// A run-ops id can only live on the new DB — skip the legacy replica entirely.
if (residency === "NEW") {
const v = await input.readNew(newClient);
return v != null ? { source: "new", value: v } : { source: "not-found" };
}
// LEGACY (or unclassifiable→LEGACY) fan-out: new first.
const v = await input.readNew(newClient);
if (v != null) {
return { source: "new", value: v };
}
// Legacy READ REPLICA only — never a legacy writer/primary (no such handle exists).
const lv = await input.readLegacy(legacyReplica);
if (lv != null) {
deps?.onLegacyReplicaRead?.(runId);
return { source: "legacy-replica", value: lv };
}
if (deps?.isPastRetention?.(runId)) {
return { source: "past-retention" };
}
return { source: "not-found" };
}
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { resolveInheritedMintKind } from "./resolveInheritedMintKind.server";
const NEW_PARENT = `run_${"a".repeat(24) + "01"}`; // run-ops id-shape -> NEW
const LEGACY_PARENT = `run_${"b".repeat(25)}`; // cuid id-shape -> LEGACY
describe("resolveInheritedMintKind (pure id-shape, shared across all mint paths)", () => {
it("inherits a run-ops (NEW) parent by id-shape -> 'runOpsId' kind", () => {
expect(resolveInheritedMintKind(NEW_PARENT)).toBe("runOpsId");
});
it("inherits a cuid (LEGACY) parent by id-shape -> cuid", () => {
expect(resolveInheritedMintKind(LEGACY_PARENT)).toBe("cuid");
});
});
@@ -0,0 +1,10 @@
import { ownerEngine } from "@trigger.dev/core/v3/isomorphic";
import type { RunIdMintKind } from "./runOpsMintKind.server";
// Mint a child in the SAME physical store as its anchor (parent run / owning batch),
// regardless of the org's current mint flag — keeps a subgraph co-resident across a
// flip. With no migration/drain, residency is a pure id-shape check (zero hot-path
// I/O): a run-ops (NEW) parent mints run-ops children, a cuid (LEGACY) parent mints cuid.
export function resolveInheritedMintKind(parentRunFriendlyId: string): RunIdMintKind {
return ownerEngine(parentRunFriendlyId) === "NEW" ? "runOpsId" : "cuid";
}
@@ -0,0 +1,81 @@
import type {
ControlPlaneResolver as EngineControlPlaneResolver,
ResolvedAuthenticatedEnv,
ResolvedEngineEnv,
ResolvedWorkerVersion,
} from "@internal/run-engine";
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
import type { ControlPlaneResolver as AppControlPlaneResolver } from "./controlPlaneResolver.server";
import { controlPlaneResolver } from "./controlPlaneResolver.server";
/**
* Adapter that presents the webapp's cross-DB cached ControlPlaneResolver as the
* run-engine `ControlPlaneResolver` seam. Injected in `runEngine.server.ts`, it replaces the
* default `PassthroughControlPlaneResolver` so the engine's dequeue/waitpoint/checkpoint/delayTTL
* reads resolve the control-plane half cache-first instead of via an in-DB join.
*
* `resolveEnv` maps the app `ResolvedEnv` (widened to carry the concurrency + nested ids the engine
* needs) onto `ResolvedEngineEnv`. `resolveWorkerVersion` forwards the env `type` so the app
* resolver runs the full run-engine dequeue dispatch (DEV most-recent / MANAGED promotion).
*/
export class RunEngineControlPlaneResolver implements EngineControlPlaneResolver {
readonly #resolver: AppControlPlaneResolver;
constructor(resolver: AppControlPlaneResolver) {
this.#resolver = resolver;
}
async resolveEnv(environmentId: string): Promise<ResolvedEngineEnv | null> {
const env = await this.#resolver.resolveEnv(environmentId);
if (!env) {
return null;
}
return {
id: env.id,
type: env.type,
archivedAt: env.archivedAt,
maximumConcurrencyLimit: env.maximumConcurrencyLimit,
concurrencyLimitBurstFactor: env.concurrencyLimitBurstFactor,
projectId: env.projectId,
organizationId: env.organizationId,
project: { id: env.projectId },
organization: { id: env.organizationId },
};
}
async resolveWorkerVersion(args: {
environmentId: string;
type: RuntimeEnvironmentType;
workerId?: string;
}): Promise<ResolvedWorkerVersion | null> {
return this.#resolver.resolveWorkerVersion({
environmentId: args.environmentId,
backgroundWorkerId: args.workerId,
type: args.type,
});
}
async resolveAuthenticatedEnv(environmentId: string): Promise<ResolvedAuthenticatedEnv | null> {
// Delegate to the cache-first, split-aware app resolver (like resolveEnv/resolveWorkerVersion):
// its authenticated-env slot now carries `git`. Keep the deleted-project guard the engine relies
// on — a deleted project's env must not resolve.
const environment = await this.#resolver.resolveAuthenticatedEnv(environmentId);
if (!environment || environment.project.deletedAt !== null) {
return null;
}
return environment;
}
async assertEnvExists(environmentId: string): Promise<void> {
await this.#resolver.assertEnvExists(environmentId);
}
}
// Module-level singleton over the app resolver singleton.
export const runEngineControlPlaneResolver = new RunEngineControlPlaneResolver(
controlPlaneResolver
);
@@ -0,0 +1,75 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { BoundedTtlCache } from "~/services/realtime/boundedTtlCache";
import { computeRunIdMintKind, type RunIdMintKind } from "./runOpsMintKind.server";
// LOCK of the CURRENT (intentional) flip-latency behavior, NOT a change request.
// resolveRunIdMintKind caches the per-org mint kind in a process-singleton
// BoundedTtlCache (TTL RUN_OPS_MINT_FLAG_CACHE_TTL_MS, 30000ms default) with get/set
// and NO invalidation hook (runOpsMintKind.server.ts:38-45,56-81). So after a flag
// flip a process keeps minting the stale kind until its cached entry expires; in
// multi-instance prod each process expires independently. This suite reconstructs the
// same flag fn over a real cache and pins both edges of that window.
// Mirror of resolveRunIdMintKind's flag fn (runOpsMintKind.server.ts:56-81).
function makeCachedFlag(
cache: BoundedTtlCache<RunIdMintKind>,
liveFlag: () => RunIdMintKind
): (orgId: string) => Promise<RunIdMintKind> {
return async (orgId: string) => {
const cached = cache.get(orgId);
if (cached !== undefined) return cached;
const kind = liveFlag();
cache.set(orgId, kind);
return kind;
};
}
const TTL_MS = 30_000;
const env = { organizationId: "org_flip", id: "env_flip" };
describe("computeRunIdMintKind flip latency (mintCache TTL window — current behavior LOCK)", () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("returns the STALE cached kind within the TTL after the flag flips 'cuid'->'runOpsId'", async () => {
const cache = new BoundedTtlCache<RunIdMintKind>(TTL_MS, 100);
let live: RunIdMintKind = "cuid";
const flag = makeCachedFlag(cache, () => live);
const deps = { masterEnabled: true, splitEnabled: async () => true, flag };
expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // populates the cache
live = "runOpsId"; // admin flips the org flag
vi.advanceTimersByTime(TTL_MS - 1); // still inside the window
expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // STALE, as designed
});
it("returns the FRESH kind once the TTL expires after a 'cuid'->'runOpsId' flip", async () => {
const cache = new BoundedTtlCache<RunIdMintKind>(TTL_MS, 100);
let live: RunIdMintKind = "cuid";
const flag = makeCachedFlag(cache, () => live);
const deps = { masterEnabled: true, splitEnabled: async () => true, flag };
expect(await computeRunIdMintKind(env, deps)).toBe("cuid");
live = "runOpsId";
vi.advanceTimersByTime(TTL_MS + 1); // past expiry -> entry evicted on read
expect(await computeRunIdMintKind(env, deps)).toBe("runOpsId"); // re-reads the live flag
});
it("symmetric flip-back 'runOpsId'->'cuid' is also stale within TTL, fresh after", async () => {
const cache = new BoundedTtlCache<RunIdMintKind>(TTL_MS, 100);
let live: RunIdMintKind = "runOpsId";
const flag = makeCachedFlag(cache, () => live);
const deps = { masterEnabled: true, splitEnabled: async () => true, flag };
expect(await computeRunIdMintKind(env, deps)).toBe("runOpsId");
live = "cuid";
vi.advanceTimersByTime(TTL_MS - 1);
expect(await computeRunIdMintKind(env, deps)).toBe("runOpsId"); // STALE
vi.advanceTimersByTime(2); // now past expiry
expect(await computeRunIdMintKind(env, deps)).toBe("cuid"); // FRESH
});
});
@@ -0,0 +1,61 @@
import { describe, expect, it, vi } from "vitest";
import { computeRunIdMintKind } from "./runOpsMintKind.server";
describe("computeRunIdMintKind (pure)", () => {
it("mints cuid when the master switch is off (never reads the flag)", async () => {
const flag = vi.fn();
const kind = await computeRunIdMintKind(
{ organizationId: "org_1", id: "env_1" },
{ masterEnabled: false, splitEnabled: async () => true, flag }
);
expect(kind).toBe("cuid");
expect(flag).not.toHaveBeenCalled();
});
it("mints cuid when split is OFF, even if master + per-org flag say 'runOpsId'", async () => {
const flag = vi.fn().mockResolvedValue("runOpsId");
const kind = await computeRunIdMintKind(
{ organizationId: "org_1", id: "env_1" },
{ masterEnabled: true, splitEnabled: async () => false, flag }
);
expect(kind).toBe("cuid"); // the split-enabled gate dominates
expect(flag).not.toHaveBeenCalled(); // split-off short-circuits before any flag read
});
it("mints run-ops id only when master on AND split on AND per-org flag = 'runOpsId'", async () => {
const flag = vi.fn().mockResolvedValue("runOpsId");
const kind = await computeRunIdMintKind(
{ organizationId: "org_1", id: "env_1" },
{ masterEnabled: true, splitEnabled: async () => true, flag }
);
expect(kind).toBe("runOpsId");
});
it("passes the already-loaded org feature flags through to the flag fn (no extra DB read)", async () => {
const flag = vi.fn().mockResolvedValue("runOpsId");
const orgFeatureFlags = { runOpsMintKind: "runOpsId" };
await computeRunIdMintKind(
{ organizationId: "org_1", id: "env_1", orgFeatureFlags },
{ masterEnabled: true, splitEnabled: async () => true, flag }
);
expect(flag).toHaveBeenCalledWith("org_1", orgFeatureFlags);
});
it("mints cuid for a non-canary org (per-org flag defaults to cuid)", async () => {
const flag = vi.fn().mockResolvedValue("cuid");
const kind = await computeRunIdMintKind(
{ organizationId: "org_2", id: "env_2" },
{ masterEnabled: true, splitEnabled: async () => true, flag }
);
expect(kind).toBe("cuid");
});
it("fails safe to cuid when the flag read throws", async () => {
const flag = vi.fn().mockRejectedValue(new Error("db down"));
const kind = await computeRunIdMintKind(
{ organizationId: "org_1", id: "env_1" },
{ masterEnabled: true, splitEnabled: async () => true, flag }
);
expect(kind).toBe("cuid"); // never arm a mint on a flag-read failure
});
});
@@ -0,0 +1,84 @@
import { $replica } from "~/db.server";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
import { BoundedTtlCache } from "~/services/realtime/boundedTtlCache";
import { singleton } from "~/utils/singleton";
import { FEATURE_FLAG } from "~/v3/featureFlags";
import { makeFlag } from "~/v3/featureFlags.server";
import { isSplitEnabled } from "./splitMode.server";
export type RunIdMintKind = "cuid" | "runOpsId";
type MintKindDeps = {
masterEnabled: boolean;
splitEnabled: () => Promise<boolean>;
// Receives the orgId + the (optional) already-loaded org feature flags. When
// orgFeatureFlags is provided, the implementation must NOT read the DB for them.
flag: (orgId: string, orgFeatureFlags: unknown | undefined) => Promise<RunIdMintKind>;
};
// PURE CORE — no env import; tests drive this directly. Gate order is load-bearing:
// master switch → split gate → per-org flag, short-circuiting at the first OFF.
export async function computeRunIdMintKind(
environment: { organizationId: string; id: string; orgFeatureFlags?: unknown },
deps: MintKindDeps
): Promise<RunIdMintKind> {
if (!deps.masterEnabled) return "cuid";
if (!(await deps.splitEnabled())) return "cuid";
try {
return await deps.flag(environment.organizationId, environment.orgFeatureFlags);
} catch (error) {
logger.error("[runOpsMintKind] flag read failed; minting cuid (fail-safe)", { error });
return "cuid";
}
}
// ENV-BOUND wrapper — the only place env/$replica/isSplitEnabled are read.
const flagFn = singleton("runOpsMintFlag", () => makeFlag($replica));
const mintCache = singleton(
"runOpsMintCache",
() =>
new BoundedTtlCache<RunIdMintKind>(
env.RUN_OPS_MINT_FLAG_CACHE_TTL_MS,
env.RUN_OPS_MINT_FLAG_CACHE_MAX_ENTRIES
)
);
export async function resolveRunIdMintKind(environment: {
organizationId: string;
id: string;
// Pass environment.organization.featureFlags from the trigger call site.
orgFeatureFlags?: unknown;
}): Promise<RunIdMintKind> {
return computeRunIdMintKind(environment, {
masterEnabled: env.RUN_OPS_MINT_ENABLED,
splitEnabled: isSplitEnabled,
flag: async (orgId, orgFeatureFlags) => {
// The cache stores only "cuid"|"runOpsId" (never undefined), so the cache's
// "stored-undefined == miss" caveat never applies here.
const cached = mintCache.get(orgId);
if (cached !== undefined) return cached;
// Hot-path pass-through: use the org flags the authenticated environment already
// carries; only fall back to a DB read when the caller did NOT pass them (non-trigger
// callers). The trigger path always passes them, so it never issues this findFirst.
const overrides =
orgFeatureFlags !== undefined
? orgFeatureFlags
: (
await $replica.organization.findFirst({
where: { id: orgId },
select: { featureFlags: true },
})
)?.featureFlags;
const kind = await flagFn({
key: FEATURE_FLAG.runOpsMintKind,
defaultValue: "cuid",
overrides: (overrides as Record<string, unknown>) ?? {},
});
mintCache.set(orgId, kind);
return kind;
},
});
}
@@ -0,0 +1,26 @@
// Pure run-ops split READ gate. The LEGACY handle is intentionally the control-plane client,
// so only the NEW client's distinctness gates (see runOpsSplitReadGate.test.ts).
export function computeRunOpsSplitReadEnabled(args: {
newReplica: unknown;
controlPlaneWriter: unknown;
controlPlaneReplica: unknown;
hasNewUrl: boolean;
hasLegacyUrl: boolean;
logger?: { warn: (msg: string, meta?: Record<string, unknown>) => void };
}): boolean {
const newIsDistinctDedicatedClient =
args.newReplica !== args.controlPlaneWriter && args.newReplica !== args.controlPlaneReplica;
const enabled = newIsDistinctDedicatedClient && args.hasNewUrl && args.hasLegacyUrl;
// Configured for split but the identity check failed: fan-out is being silently disabled.
if (!newIsDistinctDedicatedClient && args.hasNewUrl && args.hasLegacyUrl) {
args.logger?.warn(
"run-ops split read fan-out is configured (RUN_OPS_DATABASE_URL and " +
"RUN_OPS_LEGACY_DATABASE_URL are both set) but the NEW client is not a distinct " +
"instance from the control-plane client; read fan-out is silently disabled."
);
}
return enabled;
}
@@ -0,0 +1,84 @@
/**
* isSplitEnabled() is the Wave-0 gate. The entire migration/routing/FK-drop family
* MUST be unreachable when this returns false. Default is false (single-DB). Never
* infer split-vs-single from URL string-equality — distinctness is proven by the
* runtime sentinel.
*/
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
import { probeDistinctDatabases as defaultProbe } from "./distinctDbSentinel.server";
export type SplitModeConfig = {
flagEnabled: boolean;
legacyUrl?: string;
newUrl?: string;
};
export type SplitModeDeps = {
probe?: typeof defaultProbe;
logger?: { warn: (msg: string, meta?: Record<string, unknown>) => void };
};
export async function computeSplitEnabled(
config: SplitModeConfig,
deps: SplitModeDeps = {}
): Promise<boolean> {
// Hard gate #1: explicit positive opt-in. OFF by default -> never probe.
if (!config.flagEnabled) {
return false;
}
// Both URLs are required to even consider a split.
if (!config.legacyUrl || !config.newUrl) {
deps.logger?.warn(
"RUN_OPS_SPLIT_ENABLED is on but RUN_OPS_LEGACY_DATABASE_URL / RUN_OPS_DATABASE_URL are not both set; staying single-DB."
);
return false;
}
// Hard gate #2: runtime sentinel must confirm physically-distinct DBs.
const probe = deps.probe ?? defaultProbe;
const result = await probe(config.legacyUrl, config.newUrl, { logger: deps.logger });
return result.distinct === true;
}
export type SplitRealtimeInterlockConfig = {
splitEnabled: boolean;
nativeRealtimeEnabled: boolean;
};
/**
* Boot-time realtime interlock (pure predicate). Split mode puts NEW-resident
* (run-ops id) runs on the dedicated run-ops DB, but Electric replicates only from the
* control-plane DB — with the native realtime backend OFF those runs are invisible
* and every realtime subscription hangs. Refuse split unless native is on; split-off
* is always allowed regardless of the realtime backend.
*/
export function assertSplitRealtimeInterlock(config: SplitRealtimeInterlockConfig): void {
if (!config.splitEnabled) {
return;
}
if (!config.nativeRealtimeEnabled) {
throw new Error(
"RUN_OPS_SPLIT_ENABLED is on but the native realtime backend (REALTIME_BACKEND_NATIVE_ENABLED) is not enabled — Electric cannot serve NEW-resident runs; refusing to enable split."
);
}
}
let cached: Promise<boolean> | undefined;
export function isSplitEnabled(): Promise<boolean> {
if (!cached) {
cached = computeSplitEnabled(
{
flagEnabled: env.RUN_OPS_SPLIT_ENABLED,
legacyUrl: env.RUN_OPS_LEGACY_DATABASE_URL,
newUrl: env.RUN_OPS_DATABASE_URL,
},
{ logger }
);
}
return cached;
}
export function __resetSplitModeCacheForTests(): void {
cached = undefined;
}
@@ -0,0 +1,25 @@
// Pure types for the cross-seam residency guard. No runtime, no env, no Prisma.
import type { Residency } from "@trigger.dev/core/v3/isomorphic";
// Aliased (not re-declared) so it cannot drift from the classifier's own union.
export type RunOpsResidency = Residency;
export type StoreTarget = "new" | "legacy";
export type UnblockRouteKind = "MANUAL" | "DATETIME" | "RESUME_TOKEN" | "IDEMPOTENCY_REUSE" | "RUN";
export interface CrossSeamGuardInput {
waitpointId: string;
routeKind: UnblockRouteKind;
treeOwnerResidency?: RunOpsResidency;
isCrossTreeIdempotency?: boolean;
hasLegacyParent?: boolean;
}
export interface CrossSeamGuardDecision {
store: StoreTarget;
/** Always the waitpoint's OWN classification, even when pinned to legacy. */
residency: RunOpsResidency;
routeKind: UnblockRouteKind;
pinnedReason?: "non-tree-owned" | "cross-tree-idempotency" | "legacy-parent-descendant";
}
@@ -0,0 +1,96 @@
// If you add a `completeWaitpoint(` call site in the run-engine, add a matching
// entry here or `apps/webapp/test/crossSeamGuard.proof.test.ts` fails. Entries are
// one-per-textual-call-site (so the per-file count matches the source), anchored by
// method name, not line number. The `kind` is the dominant route kind — store
// selection is driven by residency, not kind, so a disputed kind label is cosmetic.
//
// PURE module — no engine import, no env, no Prisma.
import type { UnblockRouteKind } from "./types";
export interface UnblockRoute {
id: string;
kind: UnblockRouteKind;
/** The relative source path, e.g. "internal-packages/run-engine/src/engine/index.ts". */
site: string;
/** Enclosing method/symbol name — NEVER a line number. */
symbol: string;
}
const INDEX = "internal-packages/run-engine/src/engine/index.ts";
const WAITPOINT_SYSTEM = "internal-packages/run-engine/src/engine/systems/waitpointSystem.ts";
const TTL_SYSTEM = "internal-packages/run-engine/src/engine/systems/ttlSystem.ts";
const RUN_ATTEMPT_SYSTEM = "internal-packages/run-engine/src/engine/systems/runAttemptSystem.ts";
const BATCH_SYSTEM = "internal-packages/run-engine/src/engine/systems/batchSystem.ts";
export const UNBLOCK_ROUTES: readonly UnblockRoute[] = [
{
id: "index.public",
kind: "RESUME_TOKEN",
site: INDEX,
symbol: "completeWaitpoint (public declaration)",
},
{
id: "index.public.delegate",
kind: "RESUME_TOKEN",
site: INDEX,
symbol: "completeWaitpoint (delegation to waitpointSystem)",
},
{
id: "index.finishWaitpoint",
kind: "DATETIME",
site: INDEX,
symbol: "finishWaitpoint redis job",
},
{
id: "wp.sink",
kind: "RUN",
site: WAITPOINT_SYSTEM,
symbol: "completeWaitpoint (sink declaration)",
},
{
id: "wp.blockAndComplete",
kind: "RUN",
site: WAITPOINT_SYSTEM,
symbol: "blockRunAndCompleteWaitpoint",
},
{
id: "wp.getOrCreate",
kind: "IDEMPOTENCY_REUSE",
site: WAITPOINT_SYSTEM,
symbol: "getOrCreateRunWaitpoint",
},
{
id: "batch.tryCompleteBatch",
kind: "RUN",
site: BATCH_SYSTEM,
symbol: "#tryCompleteBatch",
},
{
id: "ttl.expireRun",
kind: "RUN",
site: TTL_SYSTEM,
symbol: "expireRun",
},
{
id: "runAttempt.succeeded",
kind: "RUN",
site: RUN_ATTEMPT_SYSTEM,
symbol: "attemptSucceeded",
},
{
id: "runAttempt.cancel",
kind: "RUN",
site: RUN_ATTEMPT_SYSTEM,
symbol: "cancelRun",
},
{
id: "runAttempt.permanentlyFail",
kind: "RUN",
site: RUN_ATTEMPT_SYSTEM,
symbol: "#permanentlyFailRun",
},
];
export function expectedCompleteWaitpointCallSites(): { site: string; symbol: string }[] {
return UNBLOCK_ROUTES.map((r) => ({ site: r.site, symbol: r.symbol }));
}
@@ -0,0 +1,156 @@
// A standalone wait token is minted with a cuid id and, having no owning run, is written to the
// control-plane store. Under the split topology the run-ops read replica is a distinct database
// that does not hold it, so the public token routes must fan out to the control-plane replica or
// the token is reported missing. These reads run as real queries against two containers.
import { heteroPostgresTest } from "@internal/testcontainers";
import { WaitpointId } from "@trigger.dev/core/v3/isomorphic";
import { describe, expect, vi } from "vitest";
import type { PrismaClient } from "@trigger.dev/database";
import type { PrismaReplicaClient } from "~/db.server";
import { resolveWaitpointThroughReadThrough } from "~/runEngine/concerns/resolveWaitpointThroughReadThrough.server";
import { readThroughRun } from "./readThrough.server";
vi.setConfig({ testTimeout: 60_000 });
async function seedControlPlaneEnv(prisma: PrismaClient, suffix: string) {
const organization = await prisma.organization.create({
data: { title: `Org ${suffix}`, slug: `org-${suffix}` },
});
const project = await prisma.project.create({
data: {
name: `Project ${suffix}`,
slug: `project-${suffix}`,
externalRef: `proj_${suffix}`,
organizationId: organization.id,
},
});
const environment = await prisma.runtimeEnvironment.create({
data: {
type: "PRODUCTION",
slug: `prod-${suffix}`,
projectId: project.id,
organizationId: organization.id,
apiKey: `tr_prod_${suffix}`,
pkApiKey: `pk_prod_${suffix}`,
shortcode: `short_${suffix}`,
maximumConcurrencyLimit: 10,
},
});
return { organization, project, environment };
}
async function seedStandaloneToken(prisma: PrismaClient, environmentId: string, projectId: string) {
const { id, friendlyId } = WaitpointId.generate();
await prisma.waitpoint.create({
data: {
id,
friendlyId,
type: "MANUAL",
status: "PENDING",
idempotencyKey: id,
userProvidedIdempotencyKey: false,
environmentId,
projectId,
},
});
return { id, friendlyId };
}
describe("public wait-token resolution across the split boundary", () => {
heteroPostgresTest(
"a control-plane-resident standalone token is found when the run-ops replica does not hold it",
async ({ prisma14, prisma17 }) => {
const { project, environment } = await seedControlPlaneEnv(prisma14, "token_cp");
const { id: waitpointId } = await seedStandaloneToken(prisma14, environment.id, project.id);
const waitpoint = await resolveWaitpointThroughReadThrough({
waitpointId,
environmentId: environment.id,
read: (client: PrismaReplicaClient) =>
client.waitpoint.findFirst({
where: { id: waitpointId, environmentId: environment.id },
}),
deps: {
// Pin split-on explicitly: without it the fan-out gate falls back to the
// ambient RUN_OPS_SPLIT_ENABLED env, which is unset in CI (single-DB
// passthrough reads only the run-ops replica and never fans out to the
// control-plane legacy replica that holds this cuid-shaped token).
splitEnabled: true,
newClient: prisma17 as unknown as PrismaReplicaClient,
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
},
});
expect(waitpoint).not.toBeNull();
expect(waitpoint?.id).toBe(waitpointId);
}
);
heteroPostgresTest(
"pinning both reads at the run-ops replica misses the control-plane token",
async ({ prisma14, prisma17 }) => {
const { project, environment } = await seedControlPlaneEnv(prisma14, "token_miss");
const { id: waitpointId } = await seedStandaloneToken(prisma14, environment.id, project.id);
const waitpoint = await resolveWaitpointThroughReadThrough({
waitpointId,
environmentId: environment.id,
read: (client: PrismaReplicaClient) =>
client.waitpoint.findFirst({
where: { id: waitpointId, environmentId: environment.id },
}),
deps: {
newClient: prisma17 as unknown as PrismaReplicaClient,
legacyReplica: prisma17 as unknown as PrismaReplicaClient,
// The run-ops-primary fallback is also pinned at run-ops, which does not hold this
// control-plane token, so the miss stays a miss (fallback never reads the control-plane).
newPrimary: prisma17 as unknown as PrismaReplicaClient,
},
});
expect(waitpoint).toBeNull();
}
);
heteroPostgresTest(
"the read gate forces fan-out so a control-plane token resolves while the mint flag is off",
async ({ prisma14, prisma17 }) => {
const { project, environment } = await seedControlPlaneEnv(prisma14, "token_gate");
const { id: waitpointId } = await seedStandaloneToken(prisma14, environment.id, project.id);
const read = (client: PrismaReplicaClient) =>
client.waitpoint.findFirst({
where: { id: waitpointId, environmentId: environment.id },
});
const gated = await resolveWaitpointThroughReadThrough({
waitpointId,
environmentId: environment.id,
read,
deps: {
splitEnabled: true,
newClient: prisma17 as unknown as PrismaReplicaClient,
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
},
});
expect(gated).not.toBeNull();
expect(gated?.id).toBe(waitpointId);
const passthrough = await readThroughRun({
runId: waitpointId,
environmentId: environment.id,
readNew: (c) => read(c),
readLegacy: (r) => read(r),
deps: {
splitEnabled: false,
newClient: prisma17 as unknown as PrismaReplicaClient,
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
},
});
expect(gated).not.toBeNull();
expect(passthrough.source).toBe("not-found");
}
);
});