fix: 加固 Outbox 租约与 Worker 生命周期

This commit is contained in:
root
2026-09-15 10:10:32 +08:00
parent ec573b5ae2
commit 5591d797fa
7 changed files with 502 additions and 80 deletions
+83 -15
View File
@@ -1,23 +1,91 @@
import { pathToFileURL } from "node:url";
import { PrismaClient } from "@prisma/client";
import { ModerationWorker } from "./moderation-worker.js";
const prisma = new PrismaClient();
type SignalSource = {
once(signal: "SIGTERM" | "SIGINT", listener: () => void): unknown;
removeListener(signal: "SIGTERM" | "SIGINT", listener: () => void): unknown;
};
async function main() {
await prisma.$connect();
const worker = new ModerationWorker(prisma);
if (process.argv.includes("--once")) {
await worker.runOnce();
return;
type RunWorkerOptions = {
worker: { runOnce(): Promise<boolean> };
connect: () => Promise<unknown>;
disconnect: () => Promise<unknown>;
once: boolean;
sleep?: () => Promise<unknown>;
signals?: SignalSource;
};
export async function runWorker(options: RunWorkerOptions) {
const signals = options.signals ?? process;
const sleep =
options.sleep ??
(() => new Promise((resolve) => setTimeout(resolve, 1000)));
let stopping = false;
const stop = () => {
stopping = true;
};
if (!options.once) {
signals.once("SIGTERM", stop);
signals.once("SIGINT", stop);
}
for (;;) {
const handled = await worker.runOnce();
if (!handled) await new Promise((resolve) => setTimeout(resolve, 1000));
try {
await options.connect();
do {
const handled = await options.worker.runOnce();
if (options.once || stopping) break;
if (!handled) await sleep();
} while (!stopping);
} finally {
if (!options.once) {
signals.removeListener("SIGTERM", stop);
signals.removeListener("SIGINT", stop);
}
await options.disconnect();
}
}
main()
.catch(() => {
process.exitCode = 1;
})
.finally(() => prisma.$disconnect());
export async function main() {
const prisma = new PrismaClient();
const worker = new ModerationWorker(prisma);
await runWorker({
worker,
connect: () => prisma.$connect(),
disconnect: () => prisma.$disconnect(),
once: process.argv.includes("--once"),
});
}
export function logTopLevelError(
error: unknown,
logger: Pick<Console, "error"> = console,
) {
logger.error(
JSON.stringify({
scope: "moderation-worker",
errorClass:
error instanceof Error && error.name ? error.name : "UnknownError",
}),
);
}
type TopLevelErrorOptions = {
logger?: Pick<Console, "error">;
processState?: { exitCode?: number };
};
export function handleTopLevelError(
error: unknown,
options: TopLevelErrorOptions = {},
) {
logTopLevelError(error, options.logger);
(options.processState ?? process).exitCode = 1;
}
const isEntrypoint =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(process.argv[1]).href;
if (isEntrypoint) {
main().catch(handleTopLevelError);
}