Skip to content

Commit 6c054c4

Browse files
d-csclaude
andcommitted
fix(run-ops split): invalidate control-plane cache on writes; route auth-env through the cache-first resolver
The ControlPlaneCache served env/org data with no invalidation, so admin/control-plane writes were only reflected after the TTL. Add two invalidation scopes to the cache (invalidateEnvironment for one env's slots; invalidateOrganization via a per-org epoch that env/authEnv values are stamped with, so all of an org's cached rows drop with no reverse index), expose them on the resolver, and call them at every write site that mutates cache-served data: pause/resume, archive, env/org concurrency + burst-factor, API-key regeneration, feature flags, API/batch rate limits, runs enable/disable, org + project delete, and stream-basin provisioning. Also extend the resolver's authenticated-env slot to carry `git` and make the run-engine adapter's resolveAuthenticatedEnv delegate to the cache-first, split-aware resolver instead of issuing its own $replica.findFirst, so it honors splitEnabled() and the cache like its siblings while still returning `git` and the deleted-project guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a321eaf commit 6c054c4

23 files changed

Lines changed: 477 additions & 38 deletions

.server-changes/run-ops-split-webapp-foundation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ area: webapp
33
type: feature
44
---
55

6-
Add the webapp foundation for the run-ops database split: topology/flag wiring, split-mode gating, a distinct-DB boot sentinel, and control-plane resolver read-through (all inert until `RUN_OPS_SPLIT_ENABLED`).
6+
Add the webapp foundation for the run-ops database split: topology/flag wiring, split-mode gating, a distinct-DB boot sentinel, and control-plane resolver read-through (all inert until `RUN_OPS_SPLIT_ENABLED`). The control-plane cache is now invalidated at env/org write sites (pause/resume, archive, concurrency/burst-factor, API-key regen, feature flags, rate limits, runs enable/disable, org/project delete, stream-basin provisioning) so admin/control-plane changes are reflected immediately rather than after the cache TTL, and the run-engine authenticated-env resolution goes through the cache-first, split-aware resolver.

apps/webapp/app/components/admin/backOffice/ApiRateLimitSection.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { prisma } from "~/db.server";
22
import { env } from "~/env.server";
33
import { logger } from "~/services/logger.server";
44
import { type Duration } from "~/services/rateLimiter.server";
5+
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
56
import { API_RATE_LIMIT_INTENT } from "./ApiRateLimitSection";
67
import {
78
handleRateLimitAction,
@@ -31,6 +32,8 @@ export const apiRateLimitDomain: RateLimitDomain = {
3132
where: { id: orgId },
3233
data: { apiRateLimiterConfig: next as any },
3334
});
35+
// apiRateLimiterConfig is embedded in every env of the org; drop all its cached env rows.
36+
controlPlaneResolver.invalidateOrganization(orgId);
3437
logger.info("admin.backOffice.apiRateLimit", {
3538
adminUserId,
3639
orgId,

apps/webapp/app/components/admin/backOffice/BatchRateLimitSection.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { prisma } from "~/db.server";
22
import { env } from "~/env.server";
33
import { logger } from "~/services/logger.server";
44
import { type Duration } from "~/services/rateLimiter.server";
5+
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
56
import { BATCH_RATE_LIMIT_INTENT } from "./BatchRateLimitSection";
67
import {
78
handleRateLimitAction,
@@ -31,6 +32,8 @@ export const batchRateLimitDomain: RateLimitDomain = {
3132
where: { id: orgId },
3233
data: { batchRateLimitConfig: next as any },
3334
});
35+
// batchRateLimitConfig is embedded in every env of the org; drop all its cached env rows.
36+
controlPlaneResolver.invalidateOrganization(orgId);
3437
logger.info("admin.backOffice.batchRateLimit", {
3538
adminUserId,
3639
orgId,

apps/webapp/app/models/api-key.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { RuntimeEnvironment } from "@trigger.dev/database";
22
import { prisma } from "~/db.server";
33
import { customAlphabet } from "nanoid";
44
import { RuntimeEnvironmentType } from "~/database-types";
5+
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
56

67
const apiKeyId = customAlphabet(
78
"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
@@ -87,6 +88,9 @@ export async function regenerateApiKey({ userId, environmentId }: RegenerateAPIK
8788
});
8889
});
8990

91+
// The env's apiKey changed in the control-plane; drop any cached copy.
92+
controlPlaneResolver.invalidateEnvironment(environmentId);
93+
9094
return updatedEnviroment;
9195
}
9296

apps/webapp/app/routes/admin.api.v1.environments.$environmentId.burst-factor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
22
import { z } from "zod";
33
import { prisma } from "~/db.server";
44
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
5+
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
56
import { updateEnvConcurrencyLimits } from "~/v3/runQueue.server";
67

78
const ParamsSchema = z.object({
@@ -26,5 +27,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
2627

2728
await updateEnvConcurrencyLimits(environment);
2829

30+
controlPlaneResolver.invalidateEnvironment(environmentId);
31+
2932
return json({ success: true });
3033
}

apps/webapp/app/routes/admin.api.v1.environments.$environmentId.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ActionFunctionArgs, json, LoaderFunctionArgs } from "@remix-run/server-
22
import { z } from "zod";
33
import { prisma } from "~/db.server";
44
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
5+
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
56
import { engine } from "~/v3/runEngine.server";
67
import { updateEnvConcurrencyLimits } from "~/v3/runQueue.server";
78

@@ -44,6 +45,10 @@ export async function action({ request, params }: ActionFunctionArgs) {
4445

4546
await updateEnvConcurrencyLimits(environment);
4647

48+
// Org max-concurrency changed too, which is embedded in every env of the org; invalidating
49+
// the org drops the env/authEnv rows for all of them (including this env).
50+
controlPlaneResolver.invalidateOrganization(environment.organizationId);
51+
4752
return json({ success: true });
4853
}
4954

apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.concurrency.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
22
import { z } from "zod";
33
import { prisma } from "~/db.server";
44
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
5+
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
56
import { marqs } from "~/v3/marqs/index.server";
67
import { updateEnvConcurrencyLimits } from "~/v3/runQueue.server";
78

@@ -83,5 +84,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
8384
await updateEnvConcurrencyLimits({ ...modifiedEnvironment, organization });
8485
}
8586

87+
// Org + every affected env's concurrency changed; one org invalidation covers them all.
88+
controlPlaneResolver.invalidateOrganization(organizationId);
89+
8690
return json({ success: true });
8791
}

apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.feature-flags.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ActionFunctionArgs, LoaderFunctionArgs, json } from "@remix-run/server-
22
import { z } from "zod";
33
import { prisma } from "~/db.server";
44
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
5+
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
56
import { validatePartialFeatureFlags } from "~/v3/featureFlags";
67

78
const ParamsSchema = z.object({
@@ -100,6 +101,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
100101
},
101102
});
102103

104+
// Org feature flags are embedded in every env of the org; drop all its cached env rows.
105+
controlPlaneResolver.invalidateOrganization(organizationId);
106+
103107
const updatedFlagsResult = updatedOrganization.featureFlags
104108
? validatePartialFeatureFlags(updatedOrganization.featureFlags as Record<string, unknown>)
105109
: { success: false as const };

apps/webapp/app/routes/admin.api.v1.orgs.$organizationId.runs.enable.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { z } from "zod";
1010
import { prisma } from "~/db.server";
1111
import { createEnvironment } from "~/models/organization.server";
1212
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
13+
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
1314
import { updateEnvConcurrencyLimits } from "~/v3/runQueue.server";
1415
import { PauseEnvironmentService } from "~/v3/services/pauseEnvironment.server";
1516

@@ -51,6 +52,10 @@ export async function action({ request, params }: ActionFunctionArgs) {
5152
return json({ error: "Organization not found" }, { status: 404 });
5253
}
5354

55+
// `runsEnabled` is embedded in every env of the org; drop all its cached env rows. The
56+
// per-env pause writes below invalidate their own envs via PauseEnvironmentService.
57+
controlPlaneResolver.invalidateOrganization(organizationId);
58+
5459
const environments = await prisma.runtimeEnvironment.findMany({
5560
where: {
5661
organizationId,

apps/webapp/app/routes/admin.api.v2.orgs.$organizationId.feature-flags.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Prisma } from "@trigger.dev/database";
44
import { z } from "zod";
55
import { prisma } from "~/db.server";
66
import { requireUser } from "~/services/session.server";
7+
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
78
import { flags as getGlobalFlags } from "~/v3/featureFlags.server";
89
import {
910
FEATURE_FLAG,
@@ -132,5 +133,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
132133
throw e;
133134
}
134135

136+
// Org feature flags are embedded in every env of the org; drop all its cached env rows.
137+
controlPlaneResolver.invalidateOrganization(organizationId);
138+
135139
return json({ success: true });
136140
}

0 commit comments

Comments
 (0)