chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { MollifierDrainer } from "@trigger.dev/redis-worker";
|
||||
import { prisma } from "~/db.server";
|
||||
import { env } from "~/env.server";
|
||||
import { engine as runEngine } from "~/v3/runEngine.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { singleton } from "~/utils/singleton";
|
||||
import { getMollifierBuffer } from "./mollifierBuffer.server";
|
||||
import {
|
||||
createDrainerHandler,
|
||||
createDrainerTerminalFailureHandler,
|
||||
isRetryablePgError,
|
||||
} from "./mollifierDrainerHandler.server";
|
||||
import type { MollifierSnapshot } from "./mollifierSnapshot.server";
|
||||
|
||||
// Distinct error class for the deterministic "fail loud at boot" throws
|
||||
// below. The bootstrap in `mollifierDrainerWorker.server.ts` catches
|
||||
// transient/init errors and logs them so an unrelated Redis blip doesn't
|
||||
// crash the webapp, but it RETHROWS this class — a misconfigured
|
||||
// shutdown timeout or missing buffer is a deploy-time mistake that
|
||||
// should fail health checks and roll back, not silently disable a
|
||||
// half-rolled-out feature.
|
||||
//
|
||||
// The `name` getter is set explicitly so cross-realm `instanceof` checks
|
||||
// (e.g. when Remix dev hot-reloads the module and the consumer keeps a
|
||||
// reference to the old class) can fall back to `error.name === ...` and
|
||||
// still recognise the marker.
|
||||
export class MollifierConfigurationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "MollifierConfigurationError";
|
||||
}
|
||||
}
|
||||
|
||||
function initializeMollifierDrainer(): MollifierDrainer<MollifierSnapshot> {
|
||||
const buffer = getMollifierBuffer();
|
||||
if (!buffer) {
|
||||
// Unreachable in normal config: getMollifierDrainer() gates on the
|
||||
// same env flag as getMollifierBuffer(). If we hit this, fail loud
|
||||
// — the operator has set TRIGGER_MOLLIFIER_ENABLED=1 on a worker pod but
|
||||
// the buffer can't initialise (e.g. TRIGGER_MOLLIFIER_REDIS_HOST resolves
|
||||
// to nothing). Crashing surfaces the misconfig immediately rather
|
||||
// than silently leaving entries un-drained.
|
||||
throw new MollifierConfigurationError(
|
||||
"MollifierDrainer initialised without a buffer — env vars inconsistent"
|
||||
);
|
||||
}
|
||||
|
||||
// Validate BEFORE start() so a misconfigured shutdown timeout fails
|
||||
// loud at module-load time and the singleton is never cached. If start()
|
||||
// ran first and the throw propagated out, the loop would already be
|
||||
// polling with no SIGTERM handler registered by the caller — exactly
|
||||
// the failure mode the validation is supposed to prevent.
|
||||
//
|
||||
// The SIGTERM handler in mollifierDrainerWorker.server.ts is sync fire-and-forget:
|
||||
// `drainer.stop({ timeoutMs })` returns a promise that keeps the event
|
||||
// loop alive, but in cluster mode the primary runs its own
|
||||
// GRACEFUL_SHUTDOWN_TIMEOUT and will call `process.exit(0)`
|
||||
// independently. If the drainer's deadline exceeds the primary's, the
|
||||
// drainer is cut off mid-wait — "log a warning on timeout" turns into
|
||||
// "hard exit with no log". 1s margin gives the primary room to finish
|
||||
// its own teardown after the drainer settles.
|
||||
const shutdownMarginMs = env.TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_MARGIN_MS;
|
||||
if (
|
||||
env.TRIGGER_MOLLIFIER_DRAIN_SHUTDOWN_TIMEOUT_MS >=
|
||||
env.GRACEFUL_SHUTDOWN_TIMEOUT - shutdownMarginMs
|
||||
) {
|
||||
throw new MollifierConfigurationError(
|
||||
`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.`
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug("Initializing mollifier drainer", {
|
||||
concurrency: env.TRIGGER_MOLLIFIER_DRAIN_CONCURRENCY,
|
||||
maxAttempts: env.TRIGGER_MOLLIFIER_DRAIN_MAX_ATTEMPTS,
|
||||
drainBatchSize: env.TRIGGER_MOLLIFIER_DRAIN_BATCH_SIZE,
|
||||
});
|
||||
|
||||
const drainer = new MollifierDrainer<MollifierSnapshot>({
|
||||
buffer,
|
||||
handler: createDrainerHandler({ engine: runEngine, prisma }),
|
||||
onTerminalFailure: createDrainerTerminalFailureHandler({ engine: runEngine, prisma }),
|
||||
concurrency: env.TRIGGER_MOLLIFIER_DRAIN_CONCURRENCY,
|
||||
maxAttempts: env.TRIGGER_MOLLIFIER_DRAIN_MAX_ATTEMPTS,
|
||||
maxOrgsPerTick: env.TRIGGER_MOLLIFIER_DRAIN_MAX_ORGS_PER_TICK,
|
||||
drainBatchSize: env.TRIGGER_MOLLIFIER_DRAIN_BATCH_SIZE,
|
||||
pollIntervalMs: env.TRIGGER_MOLLIFIER_DRAIN_POLL_INTERVAL_MS,
|
||||
maxBackoffMs: env.TRIGGER_MOLLIFIER_DRAIN_MAX_BACKOFF_MS,
|
||||
backoffFloorMs: env.TRIGGER_MOLLIFIER_DRAIN_BACKOFF_FLOOR_MS,
|
||||
isRetryable: isRetryablePgError,
|
||||
});
|
||||
|
||||
return drainer;
|
||||
}
|
||||
|
||||
// Returns a configured-but-stopped drainer. Callers MUST register their
|
||||
// SIGTERM / SIGINT shutdown handlers before invoking `drainer.start()` —
|
||||
// see `apps/webapp/app/v3/mollifierDrainerWorker.server.ts`. Starting
|
||||
// inside the singleton factory would put the polling loop ahead of
|
||||
// handler registration, leaving a narrow window where a SIGTERM landing
|
||||
// between `start()` and `process.once("SIGTERM", ...)` would skip the
|
||||
// graceful stop. The split is intentional.
|
||||
export function getMollifierDrainer(): MollifierDrainer<MollifierSnapshot> | null {
|
||||
if (env.TRIGGER_MOLLIFIER_ENABLED !== "1") return null;
|
||||
return singleton("mollifierDrainer", initializeMollifierDrainer);
|
||||
}
|
||||
Reference in New Issue
Block a user