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:
1 parent
68f53390f6
commit
da33cad81d
38 files changed
+1240
-105
No files matched your search
+120
-40
@@ -1,10 +1,19 @@
|
||||
import { EventType } from "@umt/contracts";
|
||||
import { OutboxRelay } from "@umt/tasks";
|
||||
import { OutboxRelay, type WorkerHandle } from "@umt/tasks";
|
||||
import type { Logger } from "pino";
|
||||
import { collectOutboxRoutes, tasksLogSink } from "../context.ts";
|
||||
import { type EventBusPort, sseConnectionCount } from "../http/sse.ts";
|
||||
import { type HeartbeatWriter, startHeartbeatWriter } from "../modules/system/heartbeat.ts";
|
||||
import type { ServerContext, ServerModule } from "../modules/types.ts";
|
||||
import {
|
||||
groupTasksByLane,
|
||||
IO_LANES,
|
||||
type IoLane,
|
||||
LaneController,
|
||||
type LaneOverrides,
|
||||
type LaneTask,
|
||||
setLaneController,
|
||||
} from "./lanes.ts";
|
||||
|
||||
/**
|
||||
* outbox → SSE 桥:这些前缀的 outbox 事件在中继投递成功后镜像到实时总线(payload 原样,带业务 id),
|
||||
@@ -16,18 +25,20 @@ export const SSE_MIRRORED_OUTBOX_TYPES: readonly string[] = Object.values(EventT
|
||||
(t) => SSE_MIRROR_PREFIXES.some((p) => t.startsWith(p)) && !SSE_MIRROR_EXCLUDED.has(t),
|
||||
);
|
||||
|
||||
/** agent 组任务:整段会话占一槽数分钟,与文件复制等 io 任务分池 */
|
||||
export const AGENT_TASKS = ["runAgent", "resolveDecision"] as const;
|
||||
/** agent 组任务:整段会话占一槽数分钟,与文件复制等 io 任务分池(以任务声明的 lane = "agent" 为准,这是兜底名单) */
|
||||
export { AGENT_TASKS } from "./lanes.ts";
|
||||
|
||||
/**
|
||||
* worker 分组:
|
||||
* all — 单个引擎 worker 执行全部任务(测试 / 省略分组时的兼容行为)
|
||||
* io — 除 AGENT_TASKS 外的全部任务 + OutboxRelay + 模块后台循环
|
||||
* agent — 只执行 AGENT_TASKS;不跑 relay 与模块循环
|
||||
* split — 同一进程里 io + agent 两个引擎 worker(--role all)
|
||||
* io — 除 agent 道外的全部任务,按 lane 分文件 / 网络 / 维护三个引擎 worker + OutboxRelay + 模块后台循环
|
||||
* agent — 只执行 agent 道任务;不跑 relay 与模块循环
|
||||
* split — 同一进程里 io 三条道 + agent 共四个引擎 worker(--role all)
|
||||
*/
|
||||
export type WorkerGroup = "all" | "io" | "agent" | "split";
|
||||
|
||||
export type PoolLabel = "all" | IoLane | "agent";
|
||||
|
||||
export interface WorkerDeps {
|
||||
logger: Logger;
|
||||
heartbeatMs: number;
|
||||
@@ -40,14 +51,21 @@ export interface WorkerDeps {
|
||||
/** 设置 outbox 中继实例到 ctx.outbox.relay */
|
||||
setRelay?: (relay: OutboxRelay) => void;
|
||||
outboxIntervalMs?: number;
|
||||
/** io 组(或 all 组)并发 */
|
||||
/** all 组(单池)并发 */
|
||||
concurrency?: number;
|
||||
/** agent 组并发 */
|
||||
agentConcurrency?: number;
|
||||
/** 缺省 all */
|
||||
group?: WorkerGroup;
|
||||
/** 注册表里的全部任务名(io 组 = 全部 − AGENT_TASKS);group 为 io/split 时必填 */
|
||||
taskNames?: readonly string[];
|
||||
/** 注册表里的全部任务(名称 + lane);group 为 io/agent/split 时必填 */
|
||||
tasks?: readonly LaneTask[];
|
||||
/** 分道:环境变量覆盖与测试用的节奏参数 */
|
||||
lanes?: {
|
||||
overrides?: LaneOverrides;
|
||||
refreshMs?: number;
|
||||
busyCacheMs?: number;
|
||||
maxYieldMs?: number;
|
||||
};
|
||||
};
|
||||
/** 事件循环延迟探针:采样间隔与告警阈值(测试可调小) */
|
||||
lagProbe?: { intervalMs?: number; warnMs?: number };
|
||||
@@ -63,31 +81,38 @@ export interface WorkerStats {
|
||||
export interface RunningWorker {
|
||||
stop(): Promise<void>;
|
||||
stats(): WorkerStats;
|
||||
/** io / split 组的分道控制器 */
|
||||
lanes: LaneController | null;
|
||||
}
|
||||
|
||||
/** 按分组拆出引擎 worker 的任务选择(versions 为任务名,匹配该任务全部版本) */
|
||||
export interface PoolPlan {
|
||||
label: PoolLabel;
|
||||
/** 任务名(匹配该任务全部版本);all 池省略 */
|
||||
versions?: string[];
|
||||
concurrency?: number;
|
||||
}
|
||||
|
||||
/** 按分组拆出引擎 worker 的任务选择;io 组按 lane 拆三条道(空道不起) */
|
||||
export function workerPlan(
|
||||
group: WorkerGroup,
|
||||
taskNames: readonly string[],
|
||||
concurrency: { io?: number; agent?: number },
|
||||
): { label: "all" | "io" | "agent"; versions?: string[]; concurrency?: number }[] {
|
||||
const agentNames = taskNames.filter((n) => (AGENT_TASKS as readonly string[]).includes(n));
|
||||
const ioNames = taskNames.filter((n) => !(AGENT_TASKS as readonly string[]).includes(n));
|
||||
const io = { label: "io" as const, versions: ioNames, ...(concurrency.io ? { concurrency: concurrency.io } : {}) };
|
||||
const agent = {
|
||||
label: "agent" as const,
|
||||
versions: agentNames,
|
||||
...(concurrency.agent ? { concurrency: concurrency.agent } : {}),
|
||||
};
|
||||
tasks: readonly LaneTask[],
|
||||
concurrency: { all?: number; agent?: number } & LaneOverrides = {},
|
||||
): PoolPlan[] {
|
||||
const { byLane } = groupTasksByLane(tasks);
|
||||
const pool = (label: IoLane | "agent"): PoolPlan[] =>
|
||||
byLane[label].length
|
||||
? [{ label, versions: byLane[label], ...(concurrency[label] ? { concurrency: concurrency[label] } : {}) }]
|
||||
: [];
|
||||
const io = IO_LANES.flatMap((l) => pool(l));
|
||||
switch (group) {
|
||||
case "all":
|
||||
return [{ label: "all", ...(concurrency.io ? { concurrency: concurrency.io } : {}) }];
|
||||
return [{ label: "all", ...(concurrency.all ? { concurrency: concurrency.all } : {}) }];
|
||||
case "io":
|
||||
return ioNames.length ? [io] : [];
|
||||
return io;
|
||||
case "agent":
|
||||
return agentNames.length ? [agent] : [];
|
||||
return pool("agent");
|
||||
case "split":
|
||||
return [...(ioNames.length ? [io] : []), ...(agentNames.length ? [agent] : [])];
|
||||
return [...io, ...pool("agent")];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,18 +159,50 @@ export async function startWorker({ logger, heartbeatMs, bus, runtime, lagProbe
|
||||
|
||||
const started: ServerModule[] = [];
|
||||
let relay: OutboxRelay | null = null;
|
||||
const plan: ReturnType<typeof workerPlan> = [];
|
||||
let lanes: LaneController | null = null;
|
||||
const plan: PoolPlan[] = [];
|
||||
const handles = new Map<PoolLabel, WorkerHandle>();
|
||||
if (runtime) {
|
||||
const { ctx } = runtime;
|
||||
if (group !== "all" && !runtime.taskNames) throw new Error(`worker 分组 ${group} 需要 taskNames`);
|
||||
plan.push(
|
||||
...workerPlan(group, runtime.taskNames ?? [], { io: runtime.concurrency, agent: runtime.agentConcurrency }),
|
||||
if (group !== "all" && !runtime.tasks) throw new Error(`worker 分组 ${group} 需要 tasks`);
|
||||
const tasks = runtime.tasks ?? [];
|
||||
plan.push(...workerPlan(group, tasks, { all: runtime.concurrency, agent: runtime.agentConcurrency }));
|
||||
const laned = plan.filter((p): p is PoolPlan & { label: IoLane } =>
|
||||
(IO_LANES as readonly string[]).includes(p.label),
|
||||
);
|
||||
for (const p of plan) {
|
||||
await ctx.engine.startWorker({
|
||||
...(p.versions ? { versions: p.versions } : {}),
|
||||
...(p.concurrency ? { concurrency: p.concurrency } : {}),
|
||||
if (laned.length) {
|
||||
const { byLane, missing } = groupTasksByLane(tasks);
|
||||
if (missing.length)
|
||||
log.warn({ tasks: missing }, "这些任务没有声明 lane,按缺省归入网络道(请在 defineTask 里补 lane)");
|
||||
lanes = new LaneController({
|
||||
engine: ctx.engine,
|
||||
logger,
|
||||
lanes: { file: byLane.file, network: byLane.network, maintenance: byLane.maintenance },
|
||||
readSettings: async () => (await ctx.settings.get("automation")).workerLanes,
|
||||
...(runtime.lanes?.overrides ? { overrides: runtime.lanes.overrides } : {}),
|
||||
...(runtime.lanes?.refreshMs ? { refreshMs: runtime.lanes.refreshMs } : {}),
|
||||
...(runtime.lanes?.busyCacheMs !== undefined ? { busyCacheMs: runtime.lanes.busyCacheMs } : {}),
|
||||
...(runtime.lanes?.maxYieldMs ? { maxYieldMs: runtime.lanes.maxYieldMs } : {}),
|
||||
});
|
||||
await lanes.start();
|
||||
setLaneController(ctx, lanes);
|
||||
}
|
||||
for (const p of plan) {
|
||||
const lane = (IO_LANES as readonly string[]).includes(p.label) ? (p.label as IoLane) : null;
|
||||
const handle =
|
||||
lane && lanes
|
||||
? await ctx.engine.startWorker({
|
||||
versions: p.versions ?? [],
|
||||
concurrency: lanes.slots(lane),
|
||||
maxActive: () => lanes?.maxActive(lane) ?? 0,
|
||||
...(lane === "maintenance" ? { claimGate: () => lanes?.maintenanceMayStart() ?? true } : {}),
|
||||
})
|
||||
: await ctx.engine.startWorker({
|
||||
...(p.versions ? { versions: p.versions } : {}),
|
||||
...(p.concurrency ? { concurrency: p.concurrency } : {}),
|
||||
});
|
||||
if (handle) handles.set(p.label, handle);
|
||||
if (lane) lanes?.attach(lane, handle || undefined);
|
||||
}
|
||||
}
|
||||
// outbox 中继与模块后台循环只在 io 组(agent 组进程只执行会话任务)
|
||||
@@ -170,12 +227,14 @@ export async function startWorker({ logger, heartbeatMs, bus, runtime, lagProbe
|
||||
}
|
||||
}
|
||||
|
||||
// 进程与健康页:每 15s 把心跳/延迟/并发/运行中 run 数写进 process_heartbeat(写失败不影响 worker)
|
||||
// 进程与健康页:每 15s 把心跳/延迟/并发/运行中 run 数写进 process_heartbeat(写失败不影响 worker);
|
||||
// io 组另写各道的并发、本进程运行中、全库排队(extra.lanes),事件循环延迟照常记录(将来拆进程的判据)
|
||||
let hbWriter: HeartbeatWriter | null = null;
|
||||
if (runtime) {
|
||||
const { ctx } = runtime;
|
||||
const groupTasks = new Set(plan.flatMap((p) => p.versions ?? []));
|
||||
const countAll = plan.some((p) => !p.versions);
|
||||
const agentPool = plan.find((p) => p.label === "agent");
|
||||
hbWriter = startHeartbeatWriter({
|
||||
db: ctx.db,
|
||||
logger,
|
||||
@@ -183,16 +242,27 @@ export async function startWorker({ logger, heartbeatMs, bus, runtime, lagProbe
|
||||
sample: async () => {
|
||||
const running = await ctx.engine.list({ status: "running", limit: 500 }).catch(() => null);
|
||||
const s = probe.stats();
|
||||
const laneStats = lanes ? await lanes.snapshot() : null;
|
||||
const concurrency: Record<string, number> = {};
|
||||
if (laneStats) {
|
||||
for (const l of laneStats) concurrency[l.lane] = l.concurrency;
|
||||
concurrency.io = laneStats.reduce((a, l) => a + l.concurrency, 0);
|
||||
} else if (plan.some((p) => p.label === "all")) {
|
||||
concurrency.io = runtime.concurrency ?? 4;
|
||||
}
|
||||
if (agentPool) concurrency.agent = handles.get("agent")?.concurrency ?? runtime.agentConcurrency ?? 4;
|
||||
return {
|
||||
concurrency: {
|
||||
...(plan.some((p) => p.label !== "agent") ? { io: runtime.concurrency ?? 4 } : {}),
|
||||
...(plan.some((p) => p.label === "agent") ? { agent: runtime.agentConcurrency ?? 4 } : {}),
|
||||
},
|
||||
concurrency,
|
||||
runningRuns: running
|
||||
? running.filter((r) => r.status === "running" && (countAll || groupTasks.has(r.taskName))).length
|
||||
: null,
|
||||
sseConnections: group === "split" ? sseConnectionCount() : null,
|
||||
extra: { group, lagWarnings: s.loopLagWarnings, probeMaxLagMs: s.maxLoopLagMs },
|
||||
extra: {
|
||||
group,
|
||||
lagWarnings: s.loopLagWarnings,
|
||||
probeMaxLagMs: s.maxLoopLagMs,
|
||||
...(laneStats ? { lanes: laneStats } : {}),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -202,7 +272,14 @@ export async function startWorker({ logger, heartbeatMs, bus, runtime, lagProbe
|
||||
{
|
||||
heartbeatMs,
|
||||
modules: started.map((m) => m.name),
|
||||
pools: plan.map((p) => ({ group: p.label, concurrency: p.concurrency ?? 4, tasks: p.versions?.length ?? "all" })),
|
||||
pools: plan.map((p) => ({
|
||||
group: p.label,
|
||||
concurrency:
|
||||
lanes && (IO_LANES as readonly string[]).includes(p.label)
|
||||
? lanes.limit(p.label as IoLane)
|
||||
: (p.concurrency ?? 4),
|
||||
tasks: p.versions?.length ?? "all",
|
||||
})),
|
||||
},
|
||||
"worker started",
|
||||
);
|
||||
@@ -210,9 +287,12 @@ export async function startWorker({ logger, heartbeatMs, bus, runtime, lagProbe
|
||||
const timer = setInterval(beat, heartbeatMs);
|
||||
return {
|
||||
stats: () => probe.stats(),
|
||||
lanes,
|
||||
async stop() {
|
||||
clearInterval(timer);
|
||||
probe.stop();
|
||||
lanes?.stop();
|
||||
if (runtime) setLaneController(runtime.ctx, null);
|
||||
await hbWriter?.stop();
|
||||
for (const m of started.reverse()) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import { DEFAULT_WORKER_LANES, WORKER_LANE_MAX, type WorkerLaneSettings } from "@umt/contracts";
|
||||
import { laneOf, type QueueCounts, type TaskDefinition, type TaskLane, type WorkerHandle } from "@umt/tasks";
|
||||
import type { Logger } from "pino";
|
||||
import type { Config } from "../config.ts";
|
||||
|
||||
// IO Worker 分道(WK-1,docs/runbook/dev.md「IO Worker 分道」):
|
||||
// io worker 进程内按任务声明的 lane 起三个引擎 worker(文件 / 网络 / 维护),各自独立并发;
|
||||
// 维护道"空闲时跑":文件道或网络道(全库)有排队或运行中的 run 时,维护道暂不领取新活(已在跑的不打断),
|
||||
// 连续让路超过 MAINTENANCE_MAX_YIELD_MS 放行一个,防止网络道长期有活时维护永远跑不上。
|
||||
// 调度器里内联执行的维护类定时任务(ScheduledJobDef.lane = "maintenance")与维护道共用并发与让路规则。
|
||||
|
||||
export const IO_LANES = ["file", "network", "maintenance"] as const;
|
||||
export type IoLane = (typeof IO_LANES)[number];
|
||||
|
||||
export const LANE_LABEL: Record<IoLane, string> = { file: "文件", network: "网络", maintenance: "维护" };
|
||||
|
||||
/** 维护道最长连续让路时长,超过即放行一个 */
|
||||
export const MAINTENANCE_MAX_YIELD_MS = 10 * 60_000;
|
||||
|
||||
export type LaneOverrides = Partial<Record<IoLane, number>>;
|
||||
|
||||
/** 环境变量覆盖:旧 WORKER_CONCURRENCY 映射到网络道(WORKER_NETWORK_CONCURRENCY 优先) */
|
||||
export function laneOverridesFromEnv(
|
||||
config: Pick<
|
||||
Config,
|
||||
"WORKER_CONCURRENCY" | "WORKER_FILE_CONCURRENCY" | "WORKER_NETWORK_CONCURRENCY" | "WORKER_MAINTENANCE_CONCURRENCY"
|
||||
>,
|
||||
): LaneOverrides {
|
||||
const network = config.WORKER_NETWORK_CONCURRENCY ?? config.WORKER_CONCURRENCY;
|
||||
return {
|
||||
...(config.WORKER_FILE_CONCURRENCY ? { file: config.WORKER_FILE_CONCURRENCY } : {}),
|
||||
...(network ? { network } : {}),
|
||||
...(config.WORKER_MAINTENANCE_CONCURRENCY ? { maintenance: config.WORKER_MAINTENANCE_CONCURRENCY } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export interface LaneTask {
|
||||
name: string;
|
||||
lane?: TaskLane;
|
||||
}
|
||||
|
||||
/** agent 会话任务:没声明 lane 时也归 agent(兼容 WK-1 前的固定名单) */
|
||||
export const AGENT_TASKS = ["runAgent", "resolveDecision"] as const;
|
||||
|
||||
/**
|
||||
* 按 lane 分组(同名多版本取第一次出现的声明);missing = 没声明 lane 的任务
|
||||
* (AGENT_TASKS 归 agent,其余按缺省归网络道)。
|
||||
*/
|
||||
export function groupTasksByLane(tasks: readonly LaneTask[]): {
|
||||
byLane: Record<TaskLane, string[]>;
|
||||
missing: string[];
|
||||
} {
|
||||
const byLane: Record<TaskLane, string[]> = { file: [], network: [], maintenance: [], agent: [] };
|
||||
const missing: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const t of tasks) {
|
||||
if (seen.has(t.name)) continue;
|
||||
seen.add(t.name);
|
||||
if (t.lane === undefined) missing.push(t.name);
|
||||
const lane =
|
||||
t.lane === undefined && (AGENT_TASKS as readonly string[]).includes(t.name)
|
||||
? "agent"
|
||||
: laneOf(t as Pick<TaskDefinition, "lane">);
|
||||
byLane[lane].push(t.name);
|
||||
}
|
||||
for (const k of Object.keys(byLane) as TaskLane[]) byLane[k].sort();
|
||||
return { byLane, missing: missing.sort() };
|
||||
}
|
||||
|
||||
export interface LaneStat {
|
||||
lane: IoLane;
|
||||
/** 当前生效的并发上限 */
|
||||
concurrency: number;
|
||||
/** 上限来源 */
|
||||
source: "env" | "settings";
|
||||
/** 本进程该道正在执行的 run 数(维护道含调度器内联的维护任务) */
|
||||
running: number;
|
||||
/** 全库该道已到点等待领取的 run 数;统计失败为 null */
|
||||
queued: number | null;
|
||||
/** 维护道:当前是否在给文件道 / 网络道让路 */
|
||||
yielding?: boolean;
|
||||
}
|
||||
|
||||
export interface LaneControllerOptions {
|
||||
engine: { queueCounts?(groups: readonly (readonly string[])[]): Promise<QueueCounts[]> };
|
||||
logger: Logger;
|
||||
/** 各道的任务名 */
|
||||
lanes: Record<IoLane, readonly string[]>;
|
||||
/** 读设置页的并发(设置读取器自带缓存,settings.changed 事件立即失效) */
|
||||
readSettings: () => Promise<WorkerLaneSettings>;
|
||||
overrides?: LaneOverrides;
|
||||
/** 重读设置的间隔,缺省 5s */
|
||||
refreshMs?: number;
|
||||
/** 忙闲判断缓存,缺省 1s */
|
||||
busyCacheMs?: number;
|
||||
maxYieldMs?: number;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
export class LaneController {
|
||||
private readonly limits: Record<IoLane, number>;
|
||||
private readonly handles: Partial<Record<IoLane, WorkerHandle>> = {};
|
||||
private inlineMaintenance = 0;
|
||||
private yieldingSince: number | null = null;
|
||||
private busyCache: { at: number; value: Promise<boolean> } | null = null;
|
||||
private timer: ReturnType<typeof setInterval> | null = null;
|
||||
private readonly log: Logger;
|
||||
private readonly now: () => number;
|
||||
|
||||
constructor(private readonly o: LaneControllerOptions) {
|
||||
this.log = o.logger.child({ component: "worker-lanes" });
|
||||
this.now = o.now ?? (() => Date.now());
|
||||
this.limits = { ...DEFAULT_WORKER_LANES, ...o.overrides };
|
||||
}
|
||||
|
||||
/** 引擎 worker 的槽位数:设置页可调到 WORKER_LANE_MAX;环境变量更大时按环境变量开 */
|
||||
slots(lane: IoLane): number {
|
||||
return Math.max(WORKER_LANE_MAX, this.o.overrides?.[lane] ?? 0);
|
||||
}
|
||||
|
||||
source(lane: IoLane): "env" | "settings" {
|
||||
return this.o.overrides?.[lane] ? "env" : "settings";
|
||||
}
|
||||
|
||||
limit(lane: IoLane): number {
|
||||
return this.limits[lane];
|
||||
}
|
||||
|
||||
tasks(lane: IoLane): readonly string[] {
|
||||
return this.o.lanes[lane];
|
||||
}
|
||||
|
||||
attach(lane: IoLane, handle: WorkerHandle | undefined): void {
|
||||
if (handle) this.handles[lane] = handle;
|
||||
}
|
||||
|
||||
/** 本进程该道正在执行的数 */
|
||||
active(lane: IoLane): number {
|
||||
const engine = this.handles[lane]?.active() ?? 0;
|
||||
return lane === "maintenance" ? engine + this.inlineMaintenance : engine;
|
||||
}
|
||||
|
||||
/** 引擎 worker 的动态上限;维护道扣掉调度器内联占用的 */
|
||||
maxActive(lane: IoLane): number {
|
||||
return lane === "maintenance" ? this.limits.maintenance - this.inlineMaintenance : this.limits[lane];
|
||||
}
|
||||
|
||||
/** 重读设置 + 环境变量覆盖;变化时记日志 */
|
||||
async refresh(): Promise<void> {
|
||||
let fromSettings: WorkerLaneSettings = DEFAULT_WORKER_LANES;
|
||||
try {
|
||||
fromSettings = { ...DEFAULT_WORKER_LANES, ...(await this.o.readSettings()) };
|
||||
} catch (err) {
|
||||
this.log.debug({ err }, "读取分道并发设置失败,沿用上次的值");
|
||||
return;
|
||||
}
|
||||
const changed: Record<string, number> = {};
|
||||
for (const lane of IO_LANES) {
|
||||
const next = Math.min(this.slots(lane), this.o.overrides?.[lane] ?? fromSettings[lane]);
|
||||
if (next !== this.limits[lane]) changed[lane] = next;
|
||||
this.limits[lane] = next;
|
||||
}
|
||||
if (Object.keys(changed).length) this.log.info({ changed, limits: { ...this.limits } }, "分道并发已更新");
|
||||
}
|
||||
|
||||
start(): Promise<void> {
|
||||
if (!this.timer) {
|
||||
this.timer = setInterval(() => void this.refresh(), this.o.refreshMs ?? 5000);
|
||||
try {
|
||||
Deno.unrefTimer(this.timer);
|
||||
} catch {
|
||||
// 非 Deno 运行时
|
||||
}
|
||||
}
|
||||
return this.refresh();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
/** 文件道或网络道(全库)有排队或运行中的 run;统计失败按空闲处理(不让维护道因统计故障永远饿着) */
|
||||
othersBusy(): Promise<boolean> {
|
||||
const now = this.now();
|
||||
if (!this.busyCache || now - this.busyCache.at >= (this.o.busyCacheMs ?? 1000)) {
|
||||
const names = [...this.o.lanes.file, ...this.o.lanes.network];
|
||||
const value = (async () => {
|
||||
if (!this.o.engine.queueCounts || names.length === 0) return false;
|
||||
try {
|
||||
const [c] = await this.o.engine.queueCounts([names]);
|
||||
return !!c && c.queued + c.running > 0;
|
||||
} catch (err) {
|
||||
this.log.debug({ err }, "分道忙闲统计失败");
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
this.busyCache = { at: now, value };
|
||||
}
|
||||
return this.busyCache.value;
|
||||
}
|
||||
|
||||
/** 维护道是否可以领取新活:其它两道空闲,或连续让路已超过上限(放行一个并重新计时) */
|
||||
async maintenanceMayStart(): Promise<boolean> {
|
||||
if (!(await this.othersBusy())) {
|
||||
this.yieldingSince = null;
|
||||
return true;
|
||||
}
|
||||
const now = this.now();
|
||||
this.yieldingSince ??= now;
|
||||
if (now - this.yieldingSince >= (this.o.maxYieldMs ?? MAINTENANCE_MAX_YIELD_MS)) {
|
||||
this.log.warn({ yieldedMs: now - this.yieldingSince }, "维护道让路超时,放行一个维护任务");
|
||||
this.yieldingSince = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
yielding(): boolean {
|
||||
return this.yieldingSince !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度器内联维护任务进入维护道:占一个维护并发;force(手动立即执行)跳过让路与上限。
|
||||
* 返回释放函数;不能进入时返回 null(任务保持到期状态,下一轮再试)。
|
||||
*/
|
||||
async enterMaintenance(force = false): Promise<(() => void) | null> {
|
||||
if (!force && this.active("maintenance") >= this.limits.maintenance) return null;
|
||||
this.inlineMaintenance += 1;
|
||||
if (!force && !(await this.maintenanceMayStart())) {
|
||||
this.inlineMaintenance -= 1;
|
||||
return null;
|
||||
}
|
||||
let released = false;
|
||||
return () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
this.inlineMaintenance -= 1;
|
||||
};
|
||||
}
|
||||
|
||||
/** 心跳 / 运行状态页用:各道并发、本进程运行中、全库排队 */
|
||||
async snapshot(): Promise<LaneStat[]> {
|
||||
let counts: (QueueCounts | undefined)[] = [];
|
||||
try {
|
||||
counts = (await this.o.engine.queueCounts?.(IO_LANES.map((l) => this.o.lanes[l]))) ?? [];
|
||||
} catch (err) {
|
||||
this.log.debug({ err }, "分道排队统计失败");
|
||||
}
|
||||
return IO_LANES.map((lane, i) => ({
|
||||
lane,
|
||||
concurrency: this.limits[lane],
|
||||
source: this.source(lane),
|
||||
running: this.active(lane),
|
||||
queued: counts[i]?.queued ?? null,
|
||||
...(lane === "maintenance" ? { yielding: this.yielding() } : {}),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 同一 ServerContext 共用一个分道控制器(调度器据此给内联维护任务让路);没起 io worker 时为空
|
||||
const controllers = new WeakMap<object, LaneController>();
|
||||
|
||||
export function setLaneController(ctx: object, c: LaneController | null): void {
|
||||
if (c) controllers.set(ctx, c);
|
||||
else controllers.delete(ctx);
|
||||
}
|
||||
|
||||
export function laneControllerOf(ctx: object): LaneController | undefined {
|
||||
return controllers.get(ctx);
|
||||
}
|
||||
Reference in new issue
Block a user