ci / 质量门禁 (push) Successful in 6m4s
背景:A6-BE-01..05 与 A6-INF-01,打通「提交 → 队列 → SSE 进度 → 完成/失败 → 取消 / 仅重试失败项」闭环,为 B3 / M1 的真实任务提供统一协议。 改动: - API media 模块:POST/GET /media-tasks、详情、SSE(Last-Event-ID 续推 + 15s 心跳 + 800ms 落库轮询)、取消(只影响未开始项)、仅重试失败项;Idempotency-Key 24h(异步提交 202;同 key 同 body 返回同一任务且不重复审计,不同 body 40902);范围外统一 404; - 队列骨架:BullMQ 5 九队列(5 组独立启停)、Outbox dispatcher(Redis 锁 + FOR UPDATE SKIP LOCKED + jobId 幂等 + 指数退避/死信)、worker 按 QUEUE 启停、demo 演练处理器(事务 + 行锁 + 取消/重试/幂等)、优雅退出与心跳; - 监控:GET /api/v1/health/queues、GET /api/v1/metrics(Prometheus 0.0.4)、infra/monitoring 告警规则与接入说明; - 通用:AuditInterceptor 支持幂等重放 skipAudit(默认行为不变)、AppModule 接线、.env.example 的 QUEUE 语义、bullmq 5.81.5 / worker @prisma/client / worker 覆盖率依赖。 影响面:OpenAPI 新增 6 个端点(openapi:diff 无破坏性变更);API 进程新增 outbox dispatcher 与 metrics/health 路由;worker 新增 BullMQ 消费;未改旧审计导出链路与既有接口。 回滚:回滚提交并停止 worker 新队列消费;task/outbox 新表数据不影响旧功能,可按需保留。 验证:api 单测 257、集成 92(连跑 2 次)、worker 27、shared 197;typecheck / lint / format:check / build / openapi:diff 全绿;本地真实 worker 启用 9 队列心跳,/health/queues、/metrics 实测,/media-tasks 未登录 401。
78 lines
2.5 KiB
TypeScript
78 lines
2.5 KiB
TypeScript
import os from 'node:os';
|
||
|
||
import { z } from 'zod';
|
||
|
||
import { buildQueueDefinitions, parseQueueSpec, type WorkerQueueDefinition } from './queues';
|
||
|
||
const envSchema = z.object({
|
||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
||
REDIS_URL: z
|
||
.string()
|
||
.min(1)
|
||
.refine(
|
||
(value) => value.startsWith('redis://') || value.startsWith('rediss://'),
|
||
'REDIS_URL 必须是 redis:// 连接串',
|
||
),
|
||
DATABASE_URL: z
|
||
.string()
|
||
.min(1)
|
||
.refine(
|
||
(value) => value.startsWith('postgresql://') || value.startsWith('postgres://'),
|
||
'DATABASE_URL 必须是 postgresql:// 连接串',
|
||
),
|
||
QUEUE: z.string().min(1).default('all'),
|
||
WORKER_CONCURRENCY: z.coerce.number().int().min(1).max(64).optional(),
|
||
WORKER_ID: z.string().min(1).max(128).optional(),
|
||
});
|
||
|
||
export interface WorkerConfig {
|
||
nodeEnv: 'development' | 'test' | 'production';
|
||
logLevel: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
||
redisUrl: string;
|
||
databaseUrl: string;
|
||
queueSpec: string;
|
||
queues: WorkerQueueDefinition[];
|
||
workerId: string;
|
||
}
|
||
|
||
function defaultWorkerId(): string {
|
||
return `${os.hostname()}-${String(process.pid)}`;
|
||
}
|
||
|
||
/**
|
||
* 加载并校验 worker 环境变量:
|
||
* - QUEUE:`all` 或逗号分隔的队列组名(script/image/video/export/verify)/ 队列名(如 media:verify);
|
||
* - WORKER_CONCURRENCY:覆盖全部队列并发;缺省取 TASK_QUEUES 中每个队列的定义值;
|
||
* - WORKER_ID:默认 hostname + pid,用于心跳与日志定位。
|
||
*/
|
||
export function loadWorkerConfig(source: NodeJS.ProcessEnv = process.env): WorkerConfig {
|
||
const result = envSchema.safeParse(source);
|
||
if (!result.success) {
|
||
const details = result.error.issues
|
||
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
||
.join('; ');
|
||
throw new Error(`Worker 环境变量校验失败:${details}`);
|
||
}
|
||
const env = result.data;
|
||
|
||
let queues: WorkerQueueDefinition[];
|
||
try {
|
||
queues = buildQueueDefinitions(parseQueueSpec(env.QUEUE), env.WORKER_CONCURRENCY);
|
||
} catch (error) {
|
||
throw new Error(
|
||
`QUEUE 配置非法(当前值 ${env.QUEUE}):${error instanceof Error ? error.message : String(error)}`,
|
||
);
|
||
}
|
||
|
||
return {
|
||
nodeEnv: env.NODE_ENV,
|
||
logLevel: env.LOG_LEVEL,
|
||
redisUrl: env.REDIS_URL,
|
||
databaseUrl: env.DATABASE_URL,
|
||
queueSpec: env.QUEUE,
|
||
queues,
|
||
workerId: env.WORKER_ID ?? defaultWorkerId(),
|
||
};
|
||
}
|