- 零第三方依赖,Node >=20 原生 ESM - 团队式编排引擎:拆解/路由/执行/审查/合并全真实 LLM - 阶段心跳、单一权威清单守卫、all-keys-failed 如实上报 - H4 会话视图/amend/watchdog 有界重试/产物区 artifacts.json - H5 零依赖三栏控制台
73 lines
3.2 KiB
JavaScript
73 lines
3.2 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|