feat(tasks): OpenWorkflow-backed TaskEngine, task registry, outbox relay and op-ledger fencing (W1-4)
- TaskEngine port + OpenWorkflowEngine ([email protected].1, schema openworkflow): enqueue/signal/cancel/get/list/startWorker/stop - defineTask registry with zod input schema; 9 stub task definitions (§3.1-8) - TaskContext: step.run/sleep/waitForFact, redacting logger, traceId, attemptToken, abortSignal driven by PG cancel flag - signal persists fact to tasks_fact first; waitForFact checks DB before waiting - OutboxRelay: FOR UPDATE SKIP LOCKED, idempotencyKey outbox:<id> - withAttemptToken fencing on tasks_op_ledger (own migration, merged in W2) - tests: kill -9 crash recovery (child worker), cancel, idempotency, concurrent relays, early fact, lease-loss fencing Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
ec023a8405
commit
1262340856
22 files changed
+1818
-8
No files matched your search
@@ -0,0 +1,77 @@
|
||||
import type { z } from "zod";
|
||||
import { UnknownTaskError } from "./errors";
|
||||
import type { RetryPolicy, TaskContext } from "./types";
|
||||
|
||||
export interface TaskDefinition<S extends z.ZodType = z.ZodType, O = unknown> {
|
||||
name: string;
|
||||
/** 正整数;不兼容改动 +1,并在 worker 上同时注册新旧版本直到旧 run 清空 */
|
||||
version: number;
|
||||
inputSchema: S;
|
||||
/** 步骤默认重试策略(步骤可单独覆盖)。外部网络类建议 5 次 1s→60s,文件类 3 次 */
|
||||
retryPolicy?: Partial<RetryPolicy>;
|
||||
handler: (ctx: TaskContext<z.output<S>>) => Promise<O>;
|
||||
}
|
||||
|
||||
export function defineTask<S extends z.ZodType, O>(def: TaskDefinition<S, O>): TaskDefinition<S, O> {
|
||||
if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(def.name)) throw new Error(`invalid task name: ${def.name}`);
|
||||
if (!Number.isInteger(def.version) || def.version < 1) throw new Error(`invalid task version: ${def.version}`);
|
||||
return def;
|
||||
}
|
||||
|
||||
export const engineVersion = (version: number) => `v${version}`;
|
||||
export const parseEngineVersion = (v: string | null): number | null => {
|
||||
const m = v ? /^v(\d+)$/.exec(v) : null;
|
||||
return m ? Number(m[1]) : null;
|
||||
};
|
||||
|
||||
export class TaskRegistry {
|
||||
private readonly defs = new Map<string, Map<number, TaskDefinition>>();
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 注册表按名存放异构的任务定义
|
||||
register(...defs: TaskDefinition<any, any>[]): this {
|
||||
for (const def of defs) {
|
||||
let byVersion = this.defs.get(def.name);
|
||||
if (!byVersion) {
|
||||
byVersion = new Map();
|
||||
this.defs.set(def.name, byVersion);
|
||||
}
|
||||
if (byVersion.has(def.version)) throw new Error(`task ${def.name}@${def.version} already registered`);
|
||||
byVersion.set(def.version, def as TaskDefinition);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
get(name: string, version: number): TaskDefinition {
|
||||
const def = this.defs.get(name)?.get(version);
|
||||
if (!def) throw new UnknownTaskError(name, version);
|
||||
return def;
|
||||
}
|
||||
|
||||
/** 新 run 一律用最高版本 */
|
||||
latest(name: string): TaskDefinition {
|
||||
const byVersion = this.defs.get(name);
|
||||
if (!byVersion || byVersion.size === 0) throw new UnknownTaskError(name);
|
||||
return byVersion.get(Math.max(...byVersion.keys()))!;
|
||||
}
|
||||
|
||||
names(): string[] {
|
||||
return [...this.defs.keys()].sort();
|
||||
}
|
||||
|
||||
all(): TaskDefinition[] {
|
||||
return [...this.defs.values()].flatMap((m) => [...m.values()]);
|
||||
}
|
||||
|
||||
/** 按 StartWorkerOptions.versions 选择:"name@2" / "@1" / "name"(该任务全部版本) */
|
||||
select(selectors?: string[]): TaskDefinition[] {
|
||||
if (!selectors || selectors.length === 0) return this.all();
|
||||
const picked = this.all().filter((d) =>
|
||||
selectors.some((s) => {
|
||||
const [name, ver] = s.split("@");
|
||||
return (!name || name === d.name) && (ver === undefined || ver === "" || Number(ver) === d.version);
|
||||
}),
|
||||
);
|
||||
if (picked.length === 0) throw new Error(`no task definitions match ${selectors.join(",")}`);
|
||||
return picked;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user