scheduler-gateway/src/recovery.js
Liuxinyu176 bbb364cd69 feat: DSH 调度网关(互联网关)v6.0.0 — 四模型合并版 R1+R2+R3
- 零第三方依赖,Node >=20 原生 ESM
- 团队式编排引擎:拆解/路由/执行/审查/合并全真实 LLM
- 阶段心跳、单一权威清单守卫、all-keys-failed 如实上报
- H4 会话视图/amend/watchdog 有界重试/产物区 artifacts.json
- H5 零依赖三栏控制台
2026-10-09 23:20:27 +08:00

73 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* R2 增强:崩溃恢复(WAL 重放)。
* 启动时若启用了 WAL(GW_WAL=1),用 recoverInFlight() 找出「已 claim 未 settle」的任务,
* 统一重投队列并记录恢复审计;重复 claim 由幂等键去重。
*/
import { TASK_STATE } from "./protocol.js";
/** 从 WAL 恢复在途任务:返回恢复的任务数。 */
export function recoverFromWal(store) {
if (!store.wal) return { recovered: 0, reason: "wal-disabled", tasks: [] };
const inFlight = store.wal.recoverInFlight();
const tasks = [];
for (const rec of inFlight) {
const t = store.tasks.get(rec.taskId);
if (!t) continue;
// 仅在仍处于 assigned/running 时重投(done/dead 不再动)
if (![TASK_STATE.ASSIGNED, TASK_STATE.RUNNING].includes(t.state)) continue;
t.state = TASK_STATE.QUEUED;
t.nodeId = null;
t.notBefore = Date.now() + 300;
t.history.push({ at: Date.now(), note: "崩溃恢复:WAL 重投(claim 未 settle)" });
store.addAudit({ kind: "task.recover", taskId: t.id, from: "wal" });
tasks.push(t);
}
store.scheduleSave?.();
return { recovered: tasks.length, reason: "wal-replay", tasks };
}
/** recovery-demo:演示「claim 后崩溃 → 重启 → WAL 恢复」。 */
export async function runRecoveryDemo({ stateRoot = null } = {}) {
const { WAL } = await import("./wal.js");
const { GatewayStore } = await import("./store.js");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { mkdtempSync, rmSync } = await import("node:fs");
const dir = stateRoot || mkdtempSync(join(tmpdir(), "gw-recovery-"));
// 阶段 1:正常运行,claim 了一些任务但没有 settle(模拟崩溃)
const wal1 = new WAL(dir, "demo");
const store1 = new GatewayStore({ persist: false, wal: wal1 });
const a = store1.createTask({ title: "recover-A" });
const b = store1.createTask({ title: "recover-B" });
const c = store1.createTask({ title: "recover-C" });
// 模拟 claim(WAL 已记录)
for (const t of [a, b]) {
store1.walAppend("task.claimed", { taskId: t.id, key: "claim:" + t.id + ":n1", data: { nodeId: "n1" } });
t.state = TASK_STATE.ASSIGNED;
t.nodeId = "n1";
}
// c 正常完成(settle 已入 WAL)
store1.walAppend("task.settled", { taskId: c.id, key: "settle:" + c.id + ":n1", data: { ok: true } });
c.state = TASK_STATE.DONE;
// 阶段 2:崩溃重启,用同一 WAL 新建 store,重放恢复
const wal2 = new WAL(dir, "demo"); // 同一 owner 重放
const store2 = new GatewayStore({ persist: false, wal: wal2 });
store2.tasks.set(a.id, { ...a, state: TASK_STATE.ASSIGNED, nodeId: "n1" });
store2.tasks.set(b.id, { ...b, state: TASK_STATE.ASSIGNED, nodeId: "n1" });
store2.tasks.set(c.id, { ...c, state: TASK_STATE.DONE });
const res = recoverFromWal(store2);
const summary = {
walFile: wal2.file,
claimedUnsettled: res.tasks.length,
recovered: res.tasks.map((t) => t.id),
requeuedStates: res.tasks.map((t) => t.state),
settledTaskUntouched: store2.tasks.get(c.id).state,
};
console.log("=== recovery-demo ===");
console.log(JSON.stringify(summary, null, 2));
if (!stateRoot) rmSync(dir, { recursive: true, force: true });
return summary;
}