Skip to content

Commit e5d403e

Browse files
committed
refactor(webapp): prefix mollifier env vars with TRIGGER_
All MOLLIFIER_* env vars renamed to TRIGGER_MOLLIFIER_*. The mollifier primitive is generic — buffer + drainer + trip evaluator with no trigger-specific assumptions at the redis-worker layer — but this PR's webapp wiring is specifically the trigger-task mollifier, with PII-sensitive payload handling and trigger-flow semantics. If we later mollify another surface (deploys, schedules, etc.) those will want their own env-var namespace; pre-prefixing now avoids a breaking rename later. Renames are mechanical: schema keys in env.server.ts, env.* references across the v3/mollifier* modules, and a handful of doc-comment mentions. The bootstrap fallback that has DRAINER_ENABLED default to the ENABLED value is updated to read TRIGGER_MOLLIFIER_ENABLED from process.env too. Code-side naming (classes, file names, the literal word "mollifier") stays unchanged — the rename is env-var only.
1 parent ad90fe3 commit e5d403e

6 files changed

Lines changed: 58 additions & 58 deletions

File tree

apps/webapp/app/env.server.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,46 +1054,46 @@ const EnvironmentSchema = z
10541054
COMMON_WORKER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"),
10551055
COMMON_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"),
10561056

1057-
MOLLIFIER_ENABLED: z.string().default("0"),
1057+
TRIGGER_MOLLIFIER_ENABLED: z.string().default("0"),
10581058
// Separate switch for the drainer (consumer side) so it can be split
10591059
// off onto a dedicated worker service. Unset → inherits
1060-
// MOLLIFIER_ENABLED, so single-container self-hosters don't have to
1060+
// TRIGGER_MOLLIFIER_ENABLED, so single-container self-hosters don't have to
10611061
// flip two switches. In multi-replica deployments, set this to "0"
10621062
// explicitly on every replica except the one dedicated drainer
10631063
// service — otherwise every replica's polling loop races for the
1064-
// same buffer entries. `MOLLIFIER_ENABLED` is still the master kill
1065-
// switch; setting this to "1" while `MOLLIFIER_ENABLED` is "0" is a
1064+
// same buffer entries. `TRIGGER_MOLLIFIER_ENABLED` is still the master kill
1065+
// switch; setting this to "1" while `TRIGGER_MOLLIFIER_ENABLED` is "0" is a
10661066
// no-op because the gate-side singleton refuses to construct a
10671067
// buffer when the system is off.
1068-
MOLLIFIER_DRAINER_ENABLED: z.string().default(process.env.MOLLIFIER_ENABLED ?? "0"),
1069-
MOLLIFIER_SHADOW_MODE: z.string().default("0"),
1070-
MOLLIFIER_REDIS_HOST: z
1068+
TRIGGER_MOLLIFIER_DRAINER_ENABLED: z.string().default(process.env.TRIGGER_MOLLIFIER_ENABLED ?? "0"),
1069+
TRIGGER_MOLLIFIER_SHADOW_MODE: z.string().default("0"),
1070+
TRIGGER_MOLLIFIER_REDIS_HOST: z
10711071
.string()
10721072
.optional()
10731073
.transform((v) => v ?? process.env.REDIS_HOST),
1074-
MOLLIFIER_REDIS_PORT: z.coerce
1074+
TRIGGER_MOLLIFIER_REDIS_PORT: z.coerce
10751075
.number()
10761076
.optional()
10771077
.transform(
10781078
(v) => v ?? (process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : undefined),
10791079
),
1080-
MOLLIFIER_REDIS_USERNAME: z
1080+
TRIGGER_MOLLIFIER_REDIS_USERNAME: z
10811081
.string()
10821082
.optional()
10831083
.transform((v) => v ?? process.env.REDIS_USERNAME),
1084-
MOLLIFIER_REDIS_PASSWORD: z
1084+
TRIGGER_MOLLIFIER_REDIS_PASSWORD: z
10851085
.string()
10861086
.optional()
10871087
.transform((v) => v ?? process.env.REDIS_PASSWORD),
1088-
MOLLIFIER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"),
1089-
MOLLIFIER_TRIP_WINDOW_MS: z.coerce.number().int().positive().default(200),
1090-
MOLLIFIER_TRIP_THRESHOLD: z.coerce.number().int().positive().default(100),
1091-
MOLLIFIER_HOLD_MS: z.coerce.number().int().positive().default(500),
1092-
MOLLIFIER_DRAIN_CONCURRENCY: z.coerce.number().int().positive().default(50),
1093-
MOLLIFIER_ENTRY_TTL_S: z.coerce.number().int().positive().default(600),
1094-
MOLLIFIER_DRAIN_MAX_ATTEMPTS: z.coerce.number().int().positive().default(3),
1095-
MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().positive().default(30_000),
1096-
MOLLIFIER_DRAIN_MAX_ORGS_PER_TICK: z.coerce.number().int().positive().default(500),
1088+
TRIGGER_MOLLIFIER_REDIS_TLS_DISABLED: z.string().default(process.env.REDIS_TLS_DISABLED ?? "false"),
1089+
TRIGGER_MOLLIFIER_TRIP_WINDOW_MS: z.coerce.number().int().positive().default(200),
1090+
TRIGGER_MOLLIFIER_TRIP_THRESHOLD: z.coerce.number().int().positive().default(100),
1091+
TRIGGER_MOLLIFIER_HOLD_MS: z.coerce.number().int().positive().default(500),
1092+
TRIGGER_MOLLIFIER_DRAIN_CONCURRENCY: z.coerce.number().int().positive().default(50),
1093+
TRIGGER_MOLLIFIER_ENTRY_TTL_S: z.coerce.number().int().positive().default(600),
1094+
TRIGGER_MOLLIFIER_DRAIN_MAX_ATTEMPTS: z.coerce.number().int().positive().default(3),
1095+
TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().positive().default(30_000),
1096+
TRIGGER_MOLLIFIER_DRAIN_MAX_ORGS_PER_TICK: z.coerce.number().int().positive().default(500),
10971097

10981098
BATCH_TRIGGER_PROCESS_JOB_VISIBILITY_TIMEOUT_MS: z.coerce
10991099
.number()

apps/webapp/app/v3/mollifier/mollifierBuffer.server.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,24 @@ export type MollifierGetBuffer = () => MollifierBuffer | null;
99

1010
function initializeMollifierBuffer(): MollifierBuffer {
1111
logger.debug("Initializing mollifier buffer", {
12-
host: env.MOLLIFIER_REDIS_HOST,
12+
host: env.TRIGGER_MOLLIFIER_REDIS_HOST,
1313
});
1414

1515
return new MollifierBuffer({
1616
redisOptions: {
1717
keyPrefix: "",
18-
host: env.MOLLIFIER_REDIS_HOST,
19-
port: env.MOLLIFIER_REDIS_PORT,
20-
username: env.MOLLIFIER_REDIS_USERNAME,
21-
password: env.MOLLIFIER_REDIS_PASSWORD,
18+
host: env.TRIGGER_MOLLIFIER_REDIS_HOST,
19+
port: env.TRIGGER_MOLLIFIER_REDIS_PORT,
20+
username: env.TRIGGER_MOLLIFIER_REDIS_USERNAME,
21+
password: env.TRIGGER_MOLLIFIER_REDIS_PASSWORD,
2222
enableAutoPipelining: true,
23-
...(env.MOLLIFIER_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }),
23+
...(env.TRIGGER_MOLLIFIER_REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }),
2424
},
25-
entryTtlSeconds: env.MOLLIFIER_ENTRY_TTL_S,
25+
entryTtlSeconds: env.TRIGGER_MOLLIFIER_ENTRY_TTL_S,
2626
});
2727
}
2828

2929
export function getMollifierBuffer(): MollifierBuffer | null {
30-
if (env.MOLLIFIER_ENABLED !== "1") return null;
30+
if (env.TRIGGER_MOLLIFIER_ENABLED !== "1") return null;
3131
return singleton("mollifierBuffer", initializeMollifierBuffer);
3232
}

apps/webapp/app/v3/mollifier/mollifierDrainer.server.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ function initializeMollifierDrainer(): MollifierDrainer<BufferedTriggerPayload>
1111
if (!buffer) {
1212
// Unreachable in normal config: getMollifierDrainer() gates on the
1313
// same env flag as getMollifierBuffer(). If we hit this, fail loud
14-
// — the operator has set MOLLIFIER_ENABLED=1 on a worker pod but
15-
// the buffer can't initialise (e.g. MOLLIFIER_REDIS_HOST resolves
14+
// — the operator has set TRIGGER_MOLLIFIER_ENABLED=1 on a worker pod but
15+
// the buffer can't initialise (e.g. TRIGGER_MOLLIFIER_REDIS_HOST resolves
1616
// to nothing). Crashing surfaces the misconfig immediately rather
1717
// than silently leaving entries un-drained.
1818
throw new Error("MollifierDrainer initialised without a buffer — env vars inconsistent");
@@ -24,7 +24,7 @@ function initializeMollifierDrainer(): MollifierDrainer<BufferedTriggerPayload>
2424
// polling with no SIGTERM handler registered by the caller — exactly
2525
// the failure mode the validation is supposed to prevent.
2626
//
27-
// The SIGTERM handler in worker.server.ts is sync fire-and-forget:
27+
// The SIGTERM handler in mollifierDrainerWorker.server.ts is sync fire-and-forget:
2828
// `drainer.stop({ timeoutMs })` returns a promise that keeps the event
2929
// loop alive, but in cluster mode the primary runs its own
3030
// GRACEFUL_SHUTDOWN_TIMEOUT and will call `process.exit(0)`
@@ -34,17 +34,17 @@ function initializeMollifierDrainer(): MollifierDrainer<BufferedTriggerPayload>
3434
// its own teardown after the drainer settles.
3535
const shutdownMarginMs = 1_000;
3636
if (
37-
env.MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS >=
37+
env.TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS >=
3838
env.GRACEFUL_SHUTDOWN_TIMEOUT - shutdownMarginMs
3939
) {
4040
throw new Error(
41-
`MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS (${env.MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS}) must be at least ${shutdownMarginMs}ms below GRACEFUL_SHUTDOWN_TIMEOUT (${env.GRACEFUL_SHUTDOWN_TIMEOUT}); otherwise the primary's hard exit shadows the drainer's deadline.`,
41+
`TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS (${env.TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS}) must be at least ${shutdownMarginMs}ms below GRACEFUL_SHUTDOWN_TIMEOUT (${env.GRACEFUL_SHUTDOWN_TIMEOUT}); otherwise the primary's hard exit shadows the drainer's deadline.`,
4242
);
4343
}
4444

4545
logger.debug("Initializing mollifier drainer", {
46-
concurrency: env.MOLLIFIER_DRAIN_CONCURRENCY,
47-
maxAttempts: env.MOLLIFIER_DRAIN_MAX_ATTEMPTS,
46+
concurrency: env.TRIGGER_MOLLIFIER_DRAIN_CONCURRENCY,
47+
maxAttempts: env.TRIGGER_MOLLIFIER_DRAIN_MAX_ATTEMPTS,
4848
});
4949

5050
// Phase 1 handler: no-op ack. The trigger has ALREADY been written to
@@ -74,9 +74,9 @@ function initializeMollifierDrainer(): MollifierDrainer<BufferedTriggerPayload>
7474
payloadHash,
7575
});
7676
},
77-
concurrency: env.MOLLIFIER_DRAIN_CONCURRENCY,
78-
maxAttempts: env.MOLLIFIER_DRAIN_MAX_ATTEMPTS,
79-
maxOrgsPerTick: env.MOLLIFIER_DRAIN_MAX_ORGS_PER_TICK,
77+
concurrency: env.TRIGGER_MOLLIFIER_DRAIN_CONCURRENCY,
78+
maxAttempts: env.TRIGGER_MOLLIFIER_DRAIN_MAX_ATTEMPTS,
79+
maxOrgsPerTick: env.TRIGGER_MOLLIFIER_DRAIN_MAX_ORGS_PER_TICK,
8080
// A no-op handler shouldn't throw, but if something does (e.g. an
8181
// unexpected deserialise failure), don't loop — let it FAIL terminally
8282
// so the entry is observable in metrics.
@@ -88,12 +88,12 @@ function initializeMollifierDrainer(): MollifierDrainer<BufferedTriggerPayload>
8888

8989
// Returns a configured-but-stopped drainer. Callers MUST register their
9090
// SIGTERM / SIGINT shutdown handlers before invoking `drainer.start()` —
91-
// see `apps/webapp/app/services/worker.server.ts`. Starting inside the
92-
// singleton factory would put the polling loop ahead of handler
93-
// registration, leaving a narrow window where a SIGTERM landing between
94-
// `start()` and `process.once("SIGTERM", ...)` would skip the graceful
95-
// stop. The split is intentional.
91+
// see `apps/webapp/app/v3/mollifierDrainerWorker.server.ts`. Starting
92+
// inside the singleton factory would put the polling loop ahead of
93+
// handler registration, leaving a narrow window where a SIGTERM landing
94+
// between `start()` and `process.once("SIGTERM", ...)` would skip the
95+
// graceful stop. The split is intentional.
9696
export function getMollifierDrainer(): MollifierDrainer<BufferedTriggerPayload> | null {
97-
if (env.MOLLIFIER_ENABLED !== "1") return null;
97+
if (env.TRIGGER_MOLLIFIER_ENABLED !== "1") return null;
9898
return singleton("mollifierDrainer", initializeMollifierDrainer);
9999
}

apps/webapp/app/v3/mollifier/mollifierGate.server.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ export type GateDependencies = {
7878
const defaultEvaluator = createRealTripEvaluator({
7979
getBuffer: () => getMollifierBuffer(),
8080
options: () => ({
81-
windowMs: env.MOLLIFIER_TRIP_WINDOW_MS,
82-
threshold: env.MOLLIFIER_TRIP_THRESHOLD,
83-
holdMs: env.MOLLIFIER_HOLD_MS,
81+
windowMs: env.TRIGGER_MOLLIFIER_TRIP_WINDOW_MS,
82+
threshold: env.TRIGGER_MOLLIFIER_TRIP_THRESHOLD,
83+
holdMs: env.TRIGGER_MOLLIFIER_HOLD_MS,
8484
}),
8585
});
8686

@@ -104,7 +104,7 @@ function logDivertDecision(
104104
// Resolve the per-org mollifier flag purely from the in-memory
105105
// `Organization.featureFlags` JSON. No DB query — `triggerTask` is the
106106
// trigger hot path and the webapp CLAUDE.md forbids adding Prisma calls
107-
// there. The fleet-wide kill switch lives in `MOLLIFIER_ENABLED`; rollout
107+
// there. The fleet-wide kill switch lives in `TRIGGER_MOLLIFIER_ENABLED`; rollout
108108
// is per-org via the JSON, matching the pattern used by `canAccessAi`,
109109
// `hasComputeAccess`, etc. There is no global `FeatureFlag` table read
110110
// in this path by design.
@@ -124,8 +124,8 @@ export function makeResolveMollifierFlag(): (inputs: GateInputs) => Promise<bool
124124
const resolveMollifierFlag = makeResolveMollifierFlag();
125125

126126
export const defaultGateDependencies: GateDependencies = {
127-
isMollifierEnabled: () => env.MOLLIFIER_ENABLED === "1",
128-
isShadowModeOn: () => env.MOLLIFIER_SHADOW_MODE === "1",
127+
isMollifierEnabled: () => env.TRIGGER_MOLLIFIER_ENABLED === "1",
128+
isShadowModeOn: () => env.TRIGGER_MOLLIFIER_SHADOW_MODE === "1",
129129
resolveOrgFlag: resolveMollifierFlag,
130130
evaluator: defaultEvaluator,
131131
logShadow: (inputs, decision) =>

apps/webapp/app/v3/mollifierDrainerWorker.server.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,18 @@ declare global {
2929
* `batchTriggerWorker`).
3030
*
3131
* Gating order:
32-
* - `MOLLIFIER_DRAINER_ENABLED !== "1"` → early return. Unset defaults
33-
* to `MOLLIFIER_ENABLED`, so single-container self-hosters still get
32+
* - `TRIGGER_MOLLIFIER_DRAINER_ENABLED !== "1"` → early return. Unset defaults
33+
* to `TRIGGER_MOLLIFIER_ENABLED`, so single-container self-hosters still get
3434
* the drainer for free with one flag. In multi-replica deployments,
3535
* set this to "0" explicitly on every replica except the dedicated
3636
* drainer service so the polling loop doesn't race across replicas.
37-
* - `MOLLIFIER_ENABLED !== "1"` → `getMollifierDrainer()` returns null
38-
* and the bootstrap is a no-op. `MOLLIFIER_ENABLED` remains the
37+
* - `TRIGGER_MOLLIFIER_ENABLED !== "1"` → `getMollifierDrainer()` returns null
38+
* and the bootstrap is a no-op. `TRIGGER_MOLLIFIER_ENABLED` remains the
3939
* master kill switch; the new flag only controls WHICH replicas
4040
* run the drainer when the system is on.
4141
*/
4242
export function initMollifierDrainerWorker(): void {
43-
if (env.MOLLIFIER_DRAINER_ENABLED !== "1") {
43+
if (env.TRIGGER_MOLLIFIER_DRAINER_ENABLED !== "1") {
4444
return;
4545
}
4646

@@ -54,7 +54,7 @@ export function initMollifierDrainerWorker(): void {
5454
// call so the two never get out of sync.
5555
const stopDrainer = () => {
5656
drainer
57-
.stop({ timeoutMs: env.MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS })
57+
.stop({ timeoutMs: env.TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS })
5858
.catch((error) => {
5959
logger.error("Failed to stop mollifier drainer", { error });
6060
});

apps/webapp/test/mollifierGate.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ describe("evaluateGate cascade — exhaustive truth table", () => {
183183
});
184184

185185
// Hot-path guard: `triggerTask.server.ts` calls `evaluateGate` on every
186-
// trigger when `MOLLIFIER_ENABLED=1`. The per-org override path must resolve
186+
// trigger when `TRIGGER_MOLLIFIER_ENABLED=1`. The per-org override path must resolve
187187
// without a Prisma round-trip — otherwise the gate adds a DB query to the
188188
// highest-throughput code path in the system (see apps/webapp/CLAUDE.md).
189189
describe("resolveMollifierFlag — hot path", () => {
@@ -211,7 +211,7 @@ describe("resolveMollifierFlag — hot path", () => {
211211
// Regression intent: the resolver MUST NOT call `flag()` (which would
212212
// query `FeatureFlag` via Prisma) on the trigger hot path. Per-org
213213
// rollout via `Organization.featureFlags` JSON is the only enable
214-
// path; the fleet-wide kill switch is `MOLLIFIER_ENABLED`.
214+
// path; the fleet-wide kill switch is `TRIGGER_MOLLIFIER_ENABLED`.
215215
const resolve = makeResolveMollifierFlag();
216216

217217
const fromNull = await resolve({
@@ -404,7 +404,7 @@ describe("evaluateGate — per-org isolation via Organization.featureFlags", ()
404404
// `FeatureFlag` table on the hot path. An org with `orgFeatureFlags`
405405
// unset (the default for almost every org during rollout) gets
406406
// pass_through, period. The fleet-wide kill switch lives in
407-
// `MOLLIFIER_ENABLED`, not the FeatureFlag table.
407+
// `TRIGGER_MOLLIFIER_ENABLED`, not the FeatureFlag table.
408408
const orgInherits = { ...inputs, orgId: "org_inherits", orgFeatureFlags: null };
409409
const orgEmpty = { ...inputs, orgId: "org_empty", orgFeatureFlags: {} };
410410
const orgUnrelated = {

0 commit comments

Comments
 (0)