feat(tasks): workers started with versions only claim their own tasks
OpenWorkflow 0.10.1 claims any run regardless of workflow name and fails unregistered ones with 5s-5m backoff; filter the claim SQL by name@version. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
cda9255083
commit
a9cee69bd2
3 files changed
+135
-2
No files matched your search
@@ -59,6 +59,54 @@ export async function migrateEngineSchema(databaseUrl: string, schema = ENGINE_S
|
||||
await backend.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 包一层只认领指定 (任务名, 版本) 的 backend:与 BackendPostgres.claimWorkflowRun 同一条 SQL(截止过期处理 +
|
||||
* SKIP LOCKED 认领),candidate 多一个 workflow_name/version 过滤。其余方法经原型链委托给原 backend。
|
||||
*/
|
||||
function claimFilteredBackend(backend: BackendPostgres, defs: readonly TaskDefinition[]): BackendPostgres {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 访问 BackendPostgres 的私有字段(pg / namespaceId / workflowRunsTable)
|
||||
const b = backend as any;
|
||||
const keys = [...new Set(defs.map((d) => `${d.name}@${engineVersion(d.version)}`))];
|
||||
const filtered = Object.create(backend) as BackendPostgres;
|
||||
Object.defineProperty(filtered, "claimWorkflowRun", {
|
||||
value: async (params: { workerId: string; leaseDurationMs: number }) => {
|
||||
const table = b.workflowRunsTable();
|
||||
const [claimed] = await b.pg`
|
||||
WITH expired AS (
|
||||
UPDATE ${table}
|
||||
SET "status" = 'failed', "error" = ${b.pg.json({ message: "Workflow run deadline exceeded" })},
|
||||
"worker_id" = NULL, "available_at" = NULL, "finished_at" = NOW(), "updated_at" = NOW()
|
||||
WHERE "namespace_id" = ${b.namespaceId}
|
||||
AND "status" IN ('pending', 'running', 'sleeping')
|
||||
AND "deadline_at" IS NOT NULL
|
||||
AND "deadline_at" <= NOW()
|
||||
RETURNING "id"
|
||||
),
|
||||
candidate AS (
|
||||
SELECT "id"
|
||||
FROM ${table}
|
||||
WHERE "namespace_id" = ${b.namespaceId}
|
||||
AND "status" IN ('pending', 'running', 'sleeping')
|
||||
AND "available_at" <= NOW()
|
||||
AND ("deadline_at" IS NULL OR "deadline_at" > NOW())
|
||||
AND ("workflow_name" || '@' || COALESCE("version", '')) = ANY(${keys}::text[])
|
||||
ORDER BY CASE WHEN "status" = 'pending' THEN 0 ELSE 1 END, "available_at", "created_at"
|
||||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
UPDATE ${table} AS wr
|
||||
SET "status" = 'running', "attempts" = "attempts" + 1, "worker_id" = ${params.workerId},
|
||||
"available_at" = NOW() + ${params.leaseDurationMs} * INTERVAL '1 millisecond',
|
||||
"started_at" = COALESCE("started_at", NOW()), "updated_at" = NOW()
|
||||
FROM candidate
|
||||
WHERE wr."id" = candidate."id" AND wr."namespace_id" = ${b.namespaceId}
|
||||
RETURNING wr.*;`;
|
||||
return claimed ?? null;
|
||||
},
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
export class OpenWorkflowEngine implements TaskEngine {
|
||||
private readonly ow: OpenWorkflow;
|
||||
private readonly workers: Worker[] = [];
|
||||
@@ -144,7 +192,10 @@ export class OpenWorkflowEngine implements TaskEngine {
|
||||
async startWorker(opts: StartWorkerOptions = {}) {
|
||||
const defs = this.opts.registry.select(opts.versions);
|
||||
const workflows = defs.map((d) => this.toWorkflow(d)) as Workflow<unknown, unknown, unknown>[];
|
||||
const worker = new Worker({ backend: this.backend, workflows, concurrency: opts.concurrency ?? 4 });
|
||||
// 指定了 versions 的 worker 只认领这些任务:OpenWorkflow 0.10.1 认领不看任务名,认领到未注册的 run 会
|
||||
// 以"未注册"失败并指数退避(5s→5m),分组 worker(io / agent)会互相拖慢对方的任务。
|
||||
const backend = opts.versions?.length ? claimFilteredBackend(this.backend, defs) : this.backend;
|
||||
const worker = new Worker({ backend, workflows, concurrency: opts.concurrency ?? 4 });
|
||||
await worker.start();
|
||||
this.workers.push(worker);
|
||||
this.log.info("worker started", { tasks: defs.map((d) => `${d.name}@${d.version}`) });
|
||||
|
||||
Reference in new issue
Block a user