Skip to content

Commit 6487461

Browse files
committed
refactor(webapp): split mollifier drainer factory into create + start
initializeMollifierDrainer() no longer calls drainer.start() — it returns a configured-but-stopped drainer. worker.server.ts init() now invokes drainer.start() AFTER the SIGTERM/SIGINT handlers are registered, gated on the same __mollifierShutdownRegistered__ guard so dev hot-reloads can't double-start. Closes the residual race window between drainer.start() (previously fired inside the singleton factory) and process.once("SIGTERM", stopDrainer) in worker.server.ts. With construction and starting separated, a signal landing during boot can never find the polling loop running without a graceful-stop path.
1 parent 9007053 commit 6487461

2 files changed

Lines changed: 16 additions & 3 deletions

File tree

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,15 @@ export async function init() {
142142
try {
143143
// getMollifierDrainer() runs the singleton factory, which validates the
144144
// shutdown-timeout reconciliation against GRACEFUL_SHUTDOWN_TIMEOUT and
145-
// throws BEFORE starting the polling loop if it's misconfigured. The
145+
// throws BEFORE constructing the drainer if it's misconfigured. The
146146
// outer catch below logs and aborts drainer registration on either that
147-
// validation error or a Redis init failure — no half-started state.
147+
// validation error or a Redis init failure — no half-started state. The
148+
// returned drainer is configured-but-stopped; start() runs below, AFTER
149+
// the SIGTERM/SIGINT handlers are registered, so a signal landing during
150+
// boot can never find the polling loop running without a graceful-stop
151+
// path. Same `__mollifierShutdownRegistered__` guard owns both the
152+
// handler registration and the start() call so dev hot-reloads don't
153+
// double-register or double-start.
148154
const drainer = getMollifierDrainer();
149155
if (drainer && !global.__mollifierShutdownRegistered__) {
150156
// The drainer owns a polling loop and a Redis client; let it drain
@@ -168,6 +174,7 @@ export async function init() {
168174
process.once("SIGTERM", stopDrainer);
169175
process.once("SIGINT", stopDrainer);
170176
global.__mollifierShutdownRegistered__ = true;
177+
drainer.start();
171178
}
172179
} catch (error) {
173180
logger.error("Failed to initialise mollifier drainer", { error });

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,16 @@ function initializeMollifierDrainer(): MollifierDrainer<BufferedTriggerPayload>
8383
isRetryable: () => false,
8484
});
8585

86-
drainer.start();
8786
return drainer;
8887
}
8988

89+
// Returns a configured-but-stopped drainer. Callers MUST register their
90+
// 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.
9096
export function getMollifierDrainer(): MollifierDrainer<BufferedTriggerPayload> | null {
9197
if (env.MOLLIFIER_ENABLED !== "1") return null;
9298
return singleton("mollifierDrainer", initializeMollifierDrainer);

0 commit comments

Comments
 (0)