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
🚀 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:
@@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user