/** * 任务推进器 —— 「过程在后端」的落点(编码规范 §9;语义出处 OS 09)。 * * 「环不依赖浏览器在线 —— 负责人关掉页面,活动照常推进」: * 运行中的任务由本 worker 一拍一拍推,直到进入等待(等人 / 等事件)或收敛终态。 * 预算与停滞熔断在 `task.tick()` 里**环外先判**,这里不重复把关。 */ import { and, eq, sql } from 'drizzle-orm'; import { taskTable } from '@modules/task'; import type { Container } from '../container.ts'; import { taskMutex } from './task-lock.ts'; export type AdvancerOptions = { readonly container: Container; /** 一轮最多推几个任务 */ readonly batchSize?: number; /** 单个任务一轮最多推几拍 —— 防止一个任务占满 worker */ readonly maxTicks?: number; }; export async function advanceOnce(options: AdvancerOptions): Promise { const { container } = options; const rows = await container.db .select({ id: taskTable.id }) .from(taskTable) .where(and(eq(taskTable.runtime, 'running'), sql`${taskTable.terminal} is null`)) .limit(options.batchSize ?? 5); let advanced = 0; for (const row of rows) { /* * 与派发器共用同一把锁:事件投递也会推进同一个任务。 * 抢不到就跳过 —— 那边正推着,这边再推就是两条线同时改一份运行时状态。 */ const ran = await taskMutex.tryRun(`task:${row.id}`, async () => { for (let tick = 0; tick < (options.maxTicks ?? 3); tick += 1) { try { const outcome = await container.task.tick(row.id as never); advanced += 1; // 进入等待或终结就停手 —— 挂起在等的任务由事件唤醒,不空转 if (outcome.task.runtime !== 'running' || outcome.task.terminal !== undefined) break; if (!outcome.progressed) break; } catch (error) { process.stderr.write( `[task-advancer] ${row.id}:${error instanceof Error ? error.message : String(error)}\n`, ); break; } } return true; }); void ran; } return advanced; } export function startTaskAdvancer(options: AdvancerOptions, intervalMs: number): { stop(): void } { let stopped = false; const timer = setInterval(() => { if (stopped) return; void advanceOnce(options).catch((error: unknown) => { process.stderr.write(`[task-advancer] ${error instanceof Error ? error.message : String(error)}\n`); }); }, intervalMs); timer.unref(); return { stop() { stopped = true; clearInterval(timer); }, }; }