Files
photo-live/services/worker/src/main.ts
T
2026-09-20 17:53:44 +08:00

91 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// services/worker/src/main.ts —— photo worker 进程入口(Task 21;P4a Task 3
// 增补水印重生成消费者)。构建产物 dist/main.js 由根 dev:worker / 生产启动
// 命令运行;env 一律来自进程环境(根 dotenv-cli 注入),此处不读文件。
// 启动顺序:Prisma + MinIO 适配器 → 三条专用 ioredis 连接(Worker 阻塞命令
// 须独占连接——photo 与 watermark 各一条;Queue/sweep 入队共用另一条)→
// BullMQ Worker ×2(photo 并发 2 / watermark 并发 1)+ 30s 补偿扫描。
// 优雅退出:SIGINT/SIGTERM 先停扫描,worker.close()(等待在飞 job)后逐个
// 关闭连接(Task 19 gotcha:BullMQ 不接管外部连接,须显式 quit;未监听的
// error 事件会崩进程,全部挂日志监听)。
import { Queue } from 'bullmq';
import IORedis from 'ioredis';
import { pino, type Logger } from 'pino';
import { PrismaClient } from '@photo/db-cn';
import { QUEUE_PHOTO_PROCESS, QUEUE_WATERMARK } from '@photo/contracts';
import { createStorageFromEnv } from '@photo/storage';
import { createPhotoProcessWorker } from './process.processor';
import { createWatermarkRegenWorker } from './watermark.processor';
import { startSweepInterval } from './sweep';
const SWEEP_INTERVAL_MS = 30_000;
function createConnection(logger: Logger, label: string): IORedis {
const connection = new IORedis(process.env.REDIS_URL_CN ?? 'redis://localhost:6379', {
maxRetriesPerRequest: null, // BullMQ 阻塞命令要求:禁用 per-request 重试上限
});
connection.on('error', (err: Error) => {
logger.warn({ label, err: err.message }, 'redis 连接异常(ioredis 自动重连)');
});
return connection;
}
async function main(): Promise<void> {
const logger = pino({ level: process.env.LOG_LEVEL ?? 'info' });
const prisma = new PrismaClient();
const storage = createStorageFromEnv();
const workerConnection = createConnection(logger, 'worker');
const watermarkConnection = createConnection(logger, 'watermark-worker');
const queueConnection = createConnection(logger, 'queue');
const sweepQueue = new Queue(QUEUE_PHOTO_PROCESS, { connection: queueConnection });
const worker = createPhotoProcessWorker({
prisma,
storage,
connection: workerConnection,
logger,
});
const watermarkWorker = createWatermarkRegenWorker({
prisma,
storage,
connection: watermarkConnection,
logger,
});
const sweep = startSweepInterval(SWEEP_INTERVAL_MS, { prisma, queue: sweepQueue, logger });
logger.info(
{
queue: QUEUE_PHOTO_PROCESS,
watermarkQueue: QUEUE_WATERMARK,
concurrency: 2,
sweepIntervalMs: SWEEP_INTERVAL_MS,
},
'photo worker started',
);
let stopping = false;
const shutdown = async (signal: string): Promise<void> => {
if (stopping) return;
stopping = true;
logger.info({ signal }, 'photo worker shutting down');
sweep.stop();
try {
await worker.close(); // 等待在飞 job 完成
await watermarkWorker.close();
await sweepQueue.close();
await workerConnection.quit().catch(() => undefined);
await watermarkConnection.quit().catch(() => undefined);
await queueConnection.quit().catch(() => undefined);
await prisma.$disconnect();
} catch (err) {
logger.error(
{ err: err instanceof Error ? err.message : String(err) },
'photo worker 关闭异常',
);
} finally {
process.exit(0);
}
};
process.once('SIGINT', () => void shutdown('SIGINT'));
process.once('SIGTERM', () => void shutdown('SIGTERM'));
}
void main();