92 lines
2.7 KiB
JavaScript
92 lines
2.7 KiB
JavaScript
/**
|
||
* 共享安全追加写入模块 (E-01/E-02 修复版)
|
||
*
|
||
* 修复:
|
||
* - 锁获取失败时重试 3 次 (10ms 间隔),而非静默降级
|
||
* - 孤儿锁检测: 锁文件超过 10 秒自动清理
|
||
* - fd 级操作减少 Windows NTFS 竞争窗口
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const LOCK_STALE_MS = 10000; // 10 秒锁超时
|
||
const LOCK_RETRIES = 3;
|
||
const LOCK_RETRY_MS = 10;
|
||
|
||
/**
|
||
* 尝试获取 O_EXCL 锁,带孤儿锁检测和重试
|
||
* @returns {number|null} 锁文件 fd 或 null
|
||
*/
|
||
function acquireAppendLock(lockFile) {
|
||
for (let attempt = 0; attempt < LOCK_RETRIES; attempt++) {
|
||
try {
|
||
return fs.openSync(lockFile, 'wx');
|
||
} catch (e) {
|
||
// 锁已存在 — 检查是否为孤儿锁
|
||
try {
|
||
const stat = fs.statSync(lockFile);
|
||
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
||
// 孤儿锁: 删除后重试
|
||
try { fs.unlinkSync(lockFile); } catch {}
|
||
continue;
|
||
}
|
||
} catch {}
|
||
// 锁被持有且未过期 — 无进程短 sleep (替代 execSync 子进程开销)
|
||
if (attempt < LOCK_RETRIES - 1) {
|
||
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
||
}
|
||
}
|
||
}
|
||
return null; // 重试耗尽
|
||
}
|
||
|
||
/**
|
||
* 安全追加一行 JSONL 到文件
|
||
* @param {string} filePath - 目标文件路径
|
||
* @param {object} entry - 要序列化的 JSON 对象
|
||
* @param {object} [opts]
|
||
* @param {boolean} [opts.useLock=false] - 是否使用文件锁
|
||
*/
|
||
function safeAppendJsonl(filePath, entry, opts = {}) {
|
||
const line = JSON.stringify(entry) + '\n';
|
||
const dir = path.dirname(filePath);
|
||
|
||
try {
|
||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||
} catch {}
|
||
|
||
if (opts.useLock) {
|
||
const lockFile = filePath + '.append.lock';
|
||
const lockFd = acquireAppendLock(lockFile);
|
||
|
||
if (lockFd !== null) {
|
||
// 持锁写入
|
||
try {
|
||
fs.appendFileSync(filePath, line);
|
||
} finally {
|
||
try { fs.closeSync(lockFd); fs.unlinkSync(lockFile); } catch {}
|
||
}
|
||
} else {
|
||
// H3 修复: 锁耗尽改 fail-close,写入 fallback 文件而非主日志,防止行交错
|
||
const fallbackFile = filePath + '.lock-failed.jsonl';
|
||
try {
|
||
process.stderr.write('[safe-append] lock exhausted for ' + path.basename(filePath) + ', wrote to fallback\n');
|
||
} catch {}
|
||
try { fs.appendFileSync(fallbackFile, line); } catch {}
|
||
}
|
||
} else {
|
||
// 无锁模式: fd 级操作
|
||
let fd;
|
||
try {
|
||
fd = fs.openSync(filePath, 'a');
|
||
fs.writeSync(fd, line);
|
||
} catch {}
|
||
finally {
|
||
if (fd !== undefined) try { fs.closeSync(fd); } catch {}
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = { safeAppendJsonl };
|