/** * 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; }