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