scheduler-gateway/src/wal.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

79 lines
2.6 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.

/**
* WAL / journal(M4):关键动作先追加 JSONL 落盘再执行;重放幂等。
* state/wal-<owner>.jsonl:{seq,ts,type,taskId,key,data}
* 重放重建:lastSeq、幂等键集合、每任务事件流(供崩溃恢复 DAG 进度)。
*/
import { openSync, appendFileSync, readFileSync, existsSync, mkdirSync, closeSync } from "node:fs";
import { resolve } from "node:path";
export class WAL {
constructor(stateRoot, owner) {
mkdirSync(stateRoot, { recursive: true });
this.owner = owner;
this.file = resolve(stateRoot, `wal-${owner.replace(/[^A-Za-z0-9_-]/g, "_")}.jsonl`);
this.seq = 0;
this.keys = new Set();
this.byTask = new Map();
this._recover();
}
_recover() {
if (!existsSync(this.file)) return;
const lines = readFileSync(this.file, "utf8").split(/\r?\n/).filter(Boolean);
for (const line of lines) {
let e;
try {
e = JSON.parse(line);
} catch {
continue; // 尾部截断行跳过
}
this.seq = Math.max(this.seq, e.seq || 0);
if (e.key) this.keys.add(e.key);
if (e.taskId) {
if (!this.byTask.has(e.taskId)) this.byTask.set(e.taskId, []);
this.byTask.get(e.taskId).push(e);
}
}
}
/** 先写 journal。key 为幂等键:重复 key 返回既有 seq,不重复落盘。 */
append(type, { taskId = null, key = null, data = {} } = {}) {
if (key && this.keys.has(key)) {
return { duplicated: true, seq: this.seq };
}
const entry = { seq: ++this.seq, ts: Date.now(), owner: this.owner, type, taskId, key, data };
const fd = openSync(this.file, "a");
try {
appendFileSync(fd, JSON.stringify(entry) + "\n");
} finally {
closeSync(fd); // 关闭触发刷盘,保证 kill -9 后可重放
}
if (key) this.keys.add(key);
if (taskId) {
if (!this.byTask.has(taskId)) this.byTask.set(taskId, []);
this.byTask.get(taskId).push(entry);
}
return { duplicated: false, entry, seq: this.seq };
}
has(key) {
return this.keys.has(key);
}
events(taskId) {
return this.byTask.get(taskId) || [];
}
/** 崩溃恢复视角:已 claim 但未 settled 的任务及其进度。 */
recoverInFlight() {
const out = [];
for (const [taskId, events] of this.byTask) {
const types = new Set(events.map((e) => e.type));
if (types.has("task.claimed") && !types.has("task.settled")) {
out.push({ taskId, events: events.map((e) => ({ type: e.type, key: e.key, data: e.data })) });
}
}
return out;
}
}