81 lines
3.3 KiB
TypeScript
81 lines
3.3 KiB
TypeScript
|
|
// 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, // 相册级任务串行起步(多相册仍并行)
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
}
|