feat(worker): watermark versioning with atomic history regeneration
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// 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, // 相册级任务串行起步(多相册仍并行)
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user