From ad90fe38acba905f2153a70bebe202d9fd7870a5 Mon Sep 17 00:00:00 2001 From: Dan Sutton Date: Fri, 15 May 2026 16:28:12 +0100 Subject: [PATCH] feat(webapp): MOLLIFIER_DRAINER_ENABLED for per-service drainer control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The drainer's polling loop has been gated on WORKER_ENABLED, which couples it to the legacy ZodWorker role. To split the drainer onto a dedicated worker service in cloud (and keep all other replicas as producer-only), introduce its own switch. Semantics: - Unset → inherits MOLLIFIER_ENABLED. Single-container self-hosters with MOLLIFIER_ENABLED=1 get the drainer for free, no second flag to remember. - Explicit MOLLIFIER_DRAINER_ENABLED=0 → drainer off on this replica. Cloud sets this everywhere except the dedicated drainer service. - Explicit MOLLIFIER_DRAINER_ENABLED=1 → drainer on, subject to MOLLIFIER_ENABLED still being the master kill switch (a drainer can't construct without the gate-side buffer singleton). The bootstrap in mollifierDrainerWorker.server.ts now gates on the new flag instead of WORKER_ENABLED, so the drainer's lifecycle is no longer coupled to the legacy worker role. --- apps/webapp/app/env.server.ts | 11 +++++++++++ .../app/v3/mollifierDrainerWorker.server.ts | 15 +++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index b0ef3e574..f0869d449 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -1055,6 +1055,17 @@ const EnvironmentSchema = z COMMON_WORKER_REDIS_CLUSTER_MODE_ENABLED: z.string().default("0"), MOLLIFIER_ENABLED: z.string().default("0"), + // Separate switch for the drainer (consumer side) so it can be split + // off onto a dedicated worker service. Unset → inherits + // MOLLIFIER_ENABLED, so single-container self-hosters don't have to + // flip two switches. In multi-replica deployments, set this to "0" + // explicitly on every replica except the one dedicated drainer + // service — otherwise every replica's polling loop races for the + // same buffer entries. `MOLLIFIER_ENABLED` is still the master kill + // switch; setting this to "1" while `MOLLIFIER_ENABLED` is "0" is a + // no-op because the gate-side singleton refuses to construct a + // buffer when the system is off. + MOLLIFIER_DRAINER_ENABLED: z.string().default(process.env.MOLLIFIER_ENABLED ?? "0"), MOLLIFIER_SHADOW_MODE: z.string().default("0"), MOLLIFIER_REDIS_HOST: z .string() diff --git a/apps/webapp/app/v3/mollifierDrainerWorker.server.ts b/apps/webapp/app/v3/mollifierDrainerWorker.server.ts index 639c9bb5d..e2e58e820 100644 --- a/apps/webapp/app/v3/mollifierDrainerWorker.server.ts +++ b/apps/webapp/app/v3/mollifierDrainerWorker.server.ts @@ -29,15 +29,18 @@ declare global { * `batchTriggerWorker`). * * Gating order: - * - `WORKER_ENABLED !== "true"` → early return (API-only replicas - * still produce into the buffer via the trigger hot path; only worker - * replicas drain it, otherwise every replica races for the same - * entries). + * - `MOLLIFIER_DRAINER_ENABLED !== "1"` → early return. Unset defaults + * to `MOLLIFIER_ENABLED`, so single-container self-hosters still get + * the drainer for free with one flag. In multi-replica deployments, + * set this to "0" explicitly on every replica except the dedicated + * drainer service so the polling loop doesn't race across replicas. * - `MOLLIFIER_ENABLED !== "1"` → `getMollifierDrainer()` returns null - * and the bootstrap is a no-op. + * and the bootstrap is a no-op. `MOLLIFIER_ENABLED` remains the + * master kill switch; the new flag only controls WHICH replicas + * run the drainer when the system is on. */ export function initMollifierDrainerWorker(): void { - if (env.WORKER_ENABLED !== "true") { + if (env.MOLLIFIER_DRAINER_ENABLED !== "1") { return; }