- 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]>
58 lines
2.6 KiB
TypeScript
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();
|
|
}
|