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

81 lines
3.3 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/watermark.processor.ts —— QUEUE_WATERMARK 消费者(P4a
// Task 3)。与 photo-process 消费者同构的 Worker 包装:
// · 照片级失败在 WatermarkRegenService 内部消化(failed 计数 + 继续),不抛错;
// · 任务级错误(完成事务乐观重试耗尽、DB 异常等)抛给 BullMQ 重试(3 次指数
// 退避 1s 起);终败判定同 process.processor(BullMQ 在 moveToFailed 之后才
// 自增 attemptsMade,当前尝试 = attemptsMade + 1),耗尽置 job 行 failed +
// errorCode 兜底;服务级中止(配置缺失等)由服务自行落 failed,不进重试。
// jobId 即 watermark_jobs.id(cuid,无 ':'——BullMQ 保留分隔符约束天然满足)。
import { Worker, type Job } from 'bullmq';
import type IORedis from 'ioredis';
import type { PrismaClient } from '@photo/db-cn';
import type { StoragePort } from '@photo/storage';
import type { Logger } from 'pino';
import { QUEUE_WATERMARK } from '@photo/contracts';
import { WatermarkRegenService, type WatermarkJobOutcome } from './watermark.service';
export interface WatermarkRegenJobData {
jobId: string;
}
/** job 名称(api 生产者同值)。 */
export const WATERMARK_REGEN_JOB_NAME = 'regen';
/** 与 api 生产者一致的重试策略。 */
export const WATERMARK_JOB_ATTEMPTS = 3;
export const WATERMARK_JOB_BACKOFF = { type: 'exponential', delay: 1000 } as const;
/** 终败 errorCode 兜底(非服务级中止码:网络/DB 等意外错误)。 */
export const WATERMARK_FALLBACK_ERROR_CODE = 'WATERMARK_REGEN_FAILED';
export interface WatermarkRegenWorkerOptions {
prisma: PrismaClient;
storage: StoragePort;
/** Worker 专用连接(阻塞命令),调用方负责 quit。 */
connection: IORedis;
concurrency?: number;
logger?: Logger;
}
export function createWatermarkRegenWorker(
options: WatermarkRegenWorkerOptions,
): Worker<WatermarkRegenJobData, WatermarkJobOutcome, string> {
const { prisma } = options;
const service = new WatermarkRegenService(prisma, options.storage, options.logger);
const processor = async (
job: Job<WatermarkRegenJobData>,
): Promise<WatermarkJobOutcome> => {
const jobId = job.data.jobId;
try {
const outcome = await service.processJob(jobId);
options.logger?.info({ jobId, outcome }, 'watermark job 完成');
return outcome;
} catch (err) {
const isFinalAttempt = job.attemptsMade + 1 >= (job.opts.attempts ?? 1);
if (isFinalAttempt) {
options.logger?.error(
{ jobId, err: err instanceof Error ? err.message : String(err) },
'watermark job 终败(重试耗尽)',
);
await prisma.watermark_jobs
.updateMany({
where: { id: jobId, state: { in: ['pending', 'running', 'failed'] } },
data: { state: 'failed', errorCode: WATERMARK_FALLBACK_ERROR_CODE },
})
.catch(() => undefined); // 落库失败仅丢审计位,不掩盖原始错误
}
throw err; // 交还 BullMQ:未耗尽则按退避重试,耗尽则入 failed set
}
};
return new Worker<WatermarkRegenJobData, WatermarkJobOutcome, string>(
QUEUE_WATERMARK,
processor,
{
connection: options.connection,
concurrency: options.concurrency ?? 1, // 相册级任务串行起步(多相册仍并行)
},
);
}