perf(webapp): read per-run environment config from the replica at dequeue (#4560)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 0s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / publish-worker-v4 (push) Has been skipped
🚀 Publish Trigger.dev Docker / scan-webapp (push) Has been skipped
🚀 Publish Trigger.dev Docker / scan-supervisor (push) Has been skipped
🦋 Changesets PR / Create Release PR (push) Has been cancelled
🚀 Publish Trigger.dev Docker / 📣 Dispatch main image (push) Has been cancelled

## Summary

Adds an opt-in path to serve a run's per-run configuration reads from
the control-plane read replica instead of the primary, reducing primary
database load during task execution. The managed-worker dequeue resolves
each run's environment, organization, and environment variables before
starting the run; those rows are stable for the life of a run, so they
can safely come from the replica.

Gated by `CONTROL_PLANE_DEQUEUE_READS_FROM_REPLICA`, defaulting to `"0"`
(reads from the primary, unchanged from today). Set it to `"1"` to route
the reads to the replica. The env-var read is scoped to the
dequeue/resolution path (`resolveVariablesForEnvironment`); dashboard
env-var reads and writes always stay on the primary. When no read
replica is configured, `$replica` transparently falls back to the
writer, so single-database self-host is unchanged either way.

Verified end-to-end against a real primary/replica split, in both
`trigger dev` and deployed (managed-worker) runs: with the flag on, env
vars inject correctly and a value set immediately before triggering a
deployed run is present on the run.
This commit is contained in:
Eric Allam
2026-08-10 17:45:57 +01:00
committed by GitHub
parent 1038641b15
commit 820c079145
5 changed files with 92 additions and 7 deletions
+1
View File
@@ -240,6 +240,7 @@ const EnvironmentSchema = z
CONTROL_PLANE_DATABASE_READ_REPLICA_URL: z.string().optional(),
CONTROL_PLANE_DATABASE_WRITER_DRIVER_ADAPTER: z.string().default("0"),
CONTROL_PLANE_DATABASE_REPLICA_DRIVER_ADAPTER: z.string().default("0"),
CONTROL_PLANE_DEQUEUE_READS_FROM_REPLICA: z.string().default("0"),
RUN_OPS_DATABASE_WRITER_DRIVER_ADAPTER: z.string().default("0"),
RUN_OPS_DATABASE_REPLICA_DRIVER_ADAPTER: z.string().default("0"),
RUN_OPS_LEGACY_DATABASE_WRITER_DRIVER_ADAPTER: z.string().default("0"),
@@ -689,10 +689,11 @@ export class EnvironmentVariablesRepository implements Repository {
async #getSecretEnvironmentVariables(
projectId: string,
environmentId: string,
parentEnvironmentId?: string
parentEnvironmentId?: string,
readFromReplica?: boolean
): Promise<EnvironmentVariable[]> {
const secretStore = getSecretStore("DATABASE", {
prismaClient: this.prismaClient,
prismaClient: readFromReplica ? this.replicaClient : this.prismaClient,
});
const parentSecrets = parentEnvironmentId
@@ -731,9 +732,15 @@ export class EnvironmentVariablesRepository implements Repository {
async getEnvironmentVariables(
projectId: string,
environmentId: string,
parentEnvironmentId?: string
parentEnvironmentId?: string,
options?: { readFromReplica?: boolean }
): Promise<EnvironmentVariable[]> {
return this.#getSecretEnvironmentVariables(projectId, environmentId, parentEnvironmentId);
return this.#getSecretEnvironmentVariables(
projectId,
environmentId,
parentEnvironmentId,
options?.readFromReplica
);
}
async delete(projectId: string, options: DeleteEnvironmentVariable): Promise<Result> {
@@ -947,7 +954,8 @@ export async function resolveVariablesForEnvironment(
let projectSecrets = await environmentVariablesRepository.getEnvironmentVariables(
runtimeEnvironment.projectId,
runtimeEnvironment.id,
parentEnvironment?.id
parentEnvironment?.id,
{ readFromReplica: env.CONTROL_PLANE_DEQUEUE_READS_FROM_REPLICA === "1" }
);
projectSecrets = renameVariables(projectSecrets, {
@@ -128,7 +128,12 @@ export interface Repository {
/**
* Return all env vars, including secret variables with values. Should only be used for executing tasks.
*/
getEnvironmentVariables(projectId: string, environmentId: string): Promise<EnvironmentVariable[]>;
getEnvironmentVariables(
projectId: string,
environmentId: string,
parentEnvironmentId?: string,
options?: { readFromReplica?: boolean }
): Promise<EnvironmentVariable[]>;
delete(projectId: string, options: DeleteEnvironmentVariable): Promise<Result>;
deleteValue(projectId: string, options: DeleteEnvironmentVariableValue): Promise<Result>;
}
@@ -538,7 +538,10 @@ export class AuthenticatedWorkerInstance extends WithRunEngine {
const defaultMachinePreset = machinePresetFromName(defaultMachine);
const environment = await this._prisma.runtimeEnvironment.findFirst({
const environmentReader =
env.CONTROL_PLANE_DEQUEUE_READS_FROM_REPLICA === "1" ? this._replica : this._prisma;
const environment = await environmentReader.runtimeEnvironment.findFirst({
where: {
id: engineResult.execution.environment.id,
},
@@ -0,0 +1,68 @@
import { setupAuthenticatedEnvironment } from "@internal/run-engine/tests";
import { containerTest } from "@internal/testcontainers";
import { describe, expect, vi } from "vitest";
import type { PrismaClient, PrismaReplicaClient } from "~/db.server";
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
type OpLog = Array<{ label: string; model?: string; operation: string }>;
function instrument<T>(client: T, label: string, log: OpLog): T {
return (client as any).$extends({
name: `spy-${label}`,
query: {
$allOperations({ model, operation, args, query }: any) {
log.push({ label, model, operation });
return query(args);
},
},
}) as T;
}
vi.setConfig({ testTimeout: 60_000 });
describe("EnvironmentVariablesRepository control-plane read routing", () => {
containerTest(
"getEnvironmentVariables reads SecretStore from the replica only when readFromReplica is set, returning the same values",
async ({ prisma }) => {
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
const projectId = environment.projectId;
const environmentId = environment.id;
await prisma.secretStore.create({
data: {
key: `environmentvariable:${projectId}:${environmentId}:MY_VAR`,
value: { secret: "hello-from-db" },
},
});
const log: OpLog = [];
const writer = instrument(prisma, "writer", log) as unknown as PrismaClient;
const replica = instrument(prisma, "replica", log) as unknown as PrismaReplicaClient;
const repository = new EnvironmentVariablesRepository(writer, replica);
log.length = 0;
const viaReplica = await repository.getEnvironmentVariables(
projectId,
environmentId,
undefined,
{
readFromReplica: true,
}
);
expect(viaReplica).toContainEqual({ key: "MY_VAR", value: "hello-from-db" });
const replicaSecretOps = log.filter((l) => l.model === "SecretStore");
expect(replicaSecretOps.length).toBeGreaterThan(0);
expect(replicaSecretOps.every((l) => l.label === "replica")).toBe(true);
expect(log.some((l) => l.model === "SecretStore" && l.label === "writer")).toBe(false);
log.length = 0;
const viaWriter = await repository.getEnvironmentVariables(projectId, environmentId);
expect(viaWriter).toContainEqual({ key: "MY_VAR", value: "hello-from-db" });
const writerSecretOps = log.filter((l) => l.model === "SecretStore");
expect(writerSecretOps.length).toBeGreaterThan(0);
expect(writerSecretOps.every((l) => l.label === "writer")).toBe(true);
}
);
});