Files
UHD_Media_Tool/packages/tasks/migrations/0001_tasks_tables.ts
T
rootandClaude Fable 5.1 1262340856 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]>
2026-09-27 17:51:21 +08:00

58 lines
2.6 KiB
TypeScript

import { type Kysely, sql } from "kysely";
// @umt/tasks 自带的三张表(W2 合并进 packages/db 迁移流程):
// - tasks_run_meta:引擎 run 的业务侧元数据(traceId/优先级/取消标志),取消标志是 abortSignal 的来源
// - tasks_fact:signal 之前先落库的事实;waitForFact 以它为准,信号只用来加速唤醒
// - tasks_op_ledger:操作账本(fencing),记录 attempt_token,旧 attempt 的提交会被拒绝
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable("tasks_run_meta")
.addColumn("run_id", "text", (c) => c.primaryKey())
.addColumn("task_name", "text", (c) => c.notNull())
.addColumn("task_version", "integer", (c) => c.notNull())
.addColumn("idempotency_key", "text", (c) => c.notNull())
.addColumn("trace_id", "text")
.addColumn("priority", "integer", (c) => c.notNull().defaultTo(0))
.addColumn("cancel_requested_at", "timestamptz")
.addColumn("cancel_reason", "text")
.addColumn("created_at", "timestamptz", (c) => c.notNull().defaultTo(sql`now()`))
.execute();
await db.schema
.createIndex("tasks_run_meta_task_idx")
.on("tasks_run_meta")
.columns(["task_name", "created_at"])
.execute();
await db.schema
.createTable("tasks_fact")
.addColumn("id", "bigserial", (c) => c.primaryKey())
.addColumn("run_id", "text", (c) => c.notNull())
.addColumn("name", "text", (c) => c.notNull())
.addColumn("payload", "jsonb")
.addColumn("created_at", "timestamptz", (c) => c.notNull().defaultTo(sql`now()`))
.execute();
await db.schema.createIndex("tasks_fact_run_name_idx").on("tasks_fact").columns(["run_id", "name", "id"]).execute();
await db.schema
.createTable("tasks_op_ledger")
.addColumn("op_key", "text", (c) => c.primaryKey())
.addColumn("run_id", "text")
.addColumn("attempt_token", "uuid", (c) => c.notNull())
.addColumn("attempts", "integer", (c) => c.notNull().defaultTo(1))
.addColumn("status", "text", (c) => c.notNull().check(sql`status in ('started','succeeded')`))
.addColumn("result", "jsonb")
.addColumn("last_error", "text")
.addColumn("started_at", "timestamptz", (c) => c.notNull().defaultTo(sql`now()`))
.addColumn("completed_at", "timestamptz")
.execute();
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable("tasks_op_ledger").ifExists().execute();
await db.schema.dropTable("tasks_fact").ifExists().execute();
await db.schema.dropTable("tasks_run_meta").ifExists().execute();
}