feat(worker): IO Worker 分道(WK-1)

io worker 进程内按任务声明的 lane 分文件 / 网络 / 维护三条道,各自并发(缺省 2/4/1)、
各自排队;维护道在文件道或网络道有排队/运行时让路(连续 10 分钟放行一个),调度器内联的
清理/回填/保留期裁剪也走维护道。并发在 设置 → 自动化 → 高级 热生效,env 覆盖;旧
WORKER_CONCURRENCY 映射到网络道。运行状态页按道显示运行中/并发/排队。归属表写进
docs/runbook/dev.md,ci 与注册表逐项核对。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 00:24:21 +08:00
1 parent 68f53390f6
commit da33cad81d
38 files changed
+1240 -105

No files matched your search

+21 -12
View File
@@ -1,12 +1,13 @@
import { readdir, stat } from "node:fs/promises";
import { checkRoot, DEFAULT_ROOT_SENTINEL, type DiskUsage, diskUsage } from "@umt/adapters";
import type {
HealthLevel,
SystemDisk,
SystemHealth,
SystemHealthHistory,
SystemMount,
SystemProcess,
import {
type HealthLevel,
type SystemDisk,
type SystemHealth,
type SystemHealthHistory,
type SystemMount,
type SystemProcess,
SystemWorkerLane,
} from "@umt/contracts";
import { type DB, type Kysely, type Selectable, sql } from "@umt/db";
import { CURRENT_HOST_WINDOW_MS, HEARTBEAT_ROW_RETENTION_MS, HEARTBEAT_STALE_MS } from "./heartbeat.ts";
@@ -61,7 +62,13 @@ function toProcess(r: HeartbeatRow, now: Date): SystemProcess {
const beat = toDate(r.last_beat_at);
const age = Math.max(0, now.getTime() - beat.getTime());
const extra = (r.extra ?? {}) as Record<string, unknown>;
const conc = (r.concurrency ?? {}) as { io?: number; agent?: number };
const conc = (r.concurrency ?? {}) as Record<string, unknown>;
const lanes = Array.isArray(extra.lanes)
? extra.lanes.flatMap((l) => {
const p = SystemWorkerLane.safeParse(l);
return p.success ? [p.data] : [];
})
: [];
const sse = extra.sseConnections;
return {
instanceId: r.instance_id,
@@ -76,10 +83,12 @@ function toProcess(r: HeartbeatRow, now: Date): SystemProcess {
stoppedAt: r.stopped_at ? toDate(r.stopped_at).toISOString() : null,
eventLoopLagMs: r.event_loop_lag_ms,
eventLoopLagMaxMs: r.event_loop_lag_max_ms,
concurrency: {
...(typeof conc.io === "number" ? { io: conc.io } : {}),
...(typeof conc.agent === "number" ? { agent: conc.agent } : {}),
},
concurrency: Object.fromEntries(
(["io", "agent", "file", "network", "maintenance"] as const).flatMap((k) =>
typeof conc[k] === "number" ? [[k, conc[k]]] : [],
),
),
lanes,
runningRuns: r.running_runs,
sseConnections: typeof sse === "number" ? sse : null,
extra,
+2 -1
View File
@@ -51,7 +51,8 @@ export function createLagSampler(intervalMs = 1000) {
}
export interface HeartbeatSample {
concurrency?: { io?: number; agent?: number };
/** io(各道合计)/ agent,以及 io 组的 file / network / maintenance */
concurrency?: Partial<Record<"io" | "agent" | "file" | "network" | "maintenance", number>>;
runningRuns?: number | null;
sseConnections?: number | null;
extra?: Record<string, unknown>;
+2
View File
@@ -185,6 +185,7 @@ export function createSystemModule(opts: SystemModuleOptions = {}): ServerModule
for (const j of RETENTION_JOBS) {
registerScheduledJob(ctx, {
key: j.jobId,
lane: "maintenance",
defaults: {
name: j.name,
description: j.description,
@@ -208,6 +209,7 @@ export function createSystemModule(opts: SystemModuleOptions = {}): ServerModule
if (everyMs <= 0) return;
registerScheduledJob(ctx, {
key: "system.heartbeat_cleanup",
lane: "maintenance",
defaults: {
name: "进程心跳清理",
description: "删除过期的进程状态记录",