Skip to content

Commit 673c7d0

Browse files
committed
fix(webapp): validate mollifier drain shutdown timeout before starting polling loop
The drainer was started inside the singleton factory, with the shutdown-timeout-vs-GRACEFUL_SHUTDOWN_TIMEOUT reconciliation living in worker.server.ts init() afterwards. If that validation threw, the polling loop was already running and the SIGTERM handler registration below it was never reached — the loop kept polling with no graceful-shutdown path, and the singleton was cached in its running state (so subsequent init() calls returned the same drainer and validation kept failing). Move the timeout check into initializeMollifierDrainer() before drainer.start(). singleton() uses ??=, so a throw inside the factory leaves the cache slot unset and the next getMollifierDrainer() call re-runs the factory — no half-started state, no missing SIGTERM handler. The catch in worker.server.ts init() still logs and aborts drainer registration on either the validation error or a Redis init failure.
1 parent 7344211 commit 673c7d0

1 file changed

Lines changed: 5 additions & 20 deletions

File tree

apps/webapp/app/services/worker.server.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -140,28 +140,13 @@ export async function init() {
140140
}
141141

142142
try {
143+
// getMollifierDrainer() runs the singleton factory, which validates the
144+
// shutdown-timeout reconciliation against GRACEFUL_SHUTDOWN_TIMEOUT and
145+
// throws BEFORE starting the polling loop if it's misconfigured. The
146+
// outer catch below logs and aborts drainer registration on either that
147+
// validation error or a Redis init failure — no half-started state.
143148
const drainer = getMollifierDrainer();
144149
if (drainer && !global.__mollifierShutdownRegistered__) {
145-
// The SIGTERM handler is sync fire-and-forget: it kicks off
146-
// `drainer.stop(...)` and returns. The unresolved promise keeps the
147-
// event loop alive, but in cluster mode the primary process runs its
148-
// own graceful-shutdown timer (`GRACEFUL_SHUTDOWN_TIMEOUT`) and will
149-
// call `process.exit(0)` independently. If the drainer's deadline
150-
// exceeds the primary's, the drainer gets cut off mid-wait — which
151-
// turns "log a warning on timeout" into "hard exit with no log".
152-
// Reconcile the two timeouts at boot rather than discovering the
153-
// misconfig from a missing warning at shutdown. Margin gives the
154-
// primary room to do its own teardown after the drainer settles.
155-
const SHUTDOWN_MARGIN_MS = 1_000;
156-
if (
157-
env.MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS >=
158-
env.GRACEFUL_SHUTDOWN_TIMEOUT - SHUTDOWN_MARGIN_MS
159-
) {
160-
throw new Error(
161-
`MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS (${env.MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS}) must be at least ${SHUTDOWN_MARGIN_MS}ms below GRACEFUL_SHUTDOWN_TIMEOUT (${env.GRACEFUL_SHUTDOWN_TIMEOUT}); otherwise the primary's hard exit shadows the drainer's deadline.`,
162-
);
163-
}
164-
165150
// The drainer owns a polling loop and a Redis client; let it drain
166151
// in-flight pops on shutdown rather than tearing the process down
167152
// mid-handler. `init()` is called per request from entry.server.tsx,

0 commit comments

Comments
 (0)