openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
461 lines (460 loc) • 17.9 kB
JavaScript
import { n as MAX_TIMER_TIMEOUT_MS } from "./number-coercion-CLj0HTDM.js";
import { t as hasErrnoCode } from "./errno-CkbDOfLk.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { c as isSqliteLockError, d as normalizeSqliteNonNegativeInteger } from "./node-sqlite-BpQX3W0e.js";
import { t as decodeMountInfoPath } from "./mountinfo-path-BCOIljp0.js";
import fs from "node:fs";
import path from "node:path";
import { AsyncLocalStorage } from "node:async_hooks";
import { performance } from "node:perf_hooks";
//#region src/infra/sqlite-wal.ts
const DEFAULT_SQLITE_WAL_AUTOCHECKPOINT_PAGES = 1e3;
const DEFAULT_SQLITE_WAL_CHECKPOINT_INTERVAL_MS = 18e5;
const DEFAULT_SQLITE_WAL_JOURNAL_SIZE_LIMIT_BYTES = 67108864;
const INCREMENTAL_VACUUM_MAX_PAGES_PER_PASS = 512;
const LINUX_NFS_SUPER_MAGIC = 26985;
const LINUX_SMB_SUPER_MAGIC = 20859;
const LINUX_CIFS_SUPER_MAGIC = 4283649346;
const LINUX_SMB2_SUPER_MAGIC = 4266872130;
const LINUX_V9FS_SUPER_MAGIC = 16914839;
const PROC_MOUNTINFO_PATH = "/proc/self/mountinfo";
const MOUNT_COMMAND_TIMEOUT_MS = 1e3;
const NETWORK_FILESYSTEM_TYPES = /* @__PURE__ */ new Set([
"cifs",
"smbfs",
"smb2",
"smb3"
]);
const CROSS_VM_FILESYSTEM_TYPES = /* @__PURE__ */ new Set([
"virtiofs",
"fuse.virtiofs",
"9p",
"9p2000.l"
]);
const JOURNAL_MODE_RETRY_INTERVAL_MS = 10;
const JOURNAL_MODE_RETRY_SLEEP = new Int32Array(new SharedArrayBuffer(4));
const PROC_SELF_FD_PATH = "/proc/self/fd";
const SQLITE_WAL_SPLIT_BRAIN_FATAL_MESSAGE = "SQLite WAL sidecar identity mismatch; terminating without SQLite cleanup";
const log = createSubsystemLogger("infra/sqlite-wal");
const runInSqliteMaintenanceContext = AsyncLocalStorage.snapshot();
function configureSqliteBusyTimeout(db, busyTimeoutMs) {
const normalizedTimeoutMs = normalizeSqliteNonNegativeInteger(busyTimeoutMs, "busyTimeoutMs");
db.exec(`PRAGMA busy_timeout = ${normalizedTimeoutMs};`);
return normalizedTimeoutMs;
}
function enableIncrementalAutoVacuumForFreshDatabase(db) {
if (db.prepare("PRAGMA page_count").get()?.page_count === 0) db.exec("PRAGMA auto_vacuum = INCREMENTAL;");
}
/**
* Configure lock retry before inspecting or mutating a fresh database header.
* Concurrent first opens can otherwise fail before schema transactions begin.
*/
function configureSqlitePreSchemaPragmas(db, options = {}) {
if (options.busyTimeoutMs !== void 0) configureSqliteBusyTimeout(db, options.busyTimeoutMs);
enableIncrementalAutoVacuumForFreshDatabase(db);
}
function findExistingVolumePaths(targetPath) {
let current = path.resolve(targetPath);
while (true) {
let stats;
try {
stats = fs.statSync(current);
} catch {
const parent = path.dirname(current);
if (parent === current) return null;
current = parent;
continue;
}
const existingPath = fs.realpathSync(current);
return {
canonicalPath: stats.isDirectory() ? existingPath : path.dirname(existingPath),
originalPath: stats.isDirectory() ? current : path.dirname(current)
};
}
}
function parseProcMountInfoEntries(contents) {
const entries = [];
for (const line of contents.split("\n")) {
const separator = line.indexOf(" - ");
if (separator === -1) continue;
const fields = line.slice(0, separator).split(" ");
const suffixFields = line.slice(separator + 3).split(" ");
const mountPoint = fields[4];
const fsType = suffixFields[0];
if (mountPoint && fsType) entries.push({
mountPoint: decodeMountInfoPath(mountPoint),
fsType,
...suffixFields[1] ? { source: decodeMountInfoPath(suffixFields[1]) } : {}
});
}
return entries;
}
function parseMountCommandEntries(contents) {
const entries = [];
for (const line of contents.split("\n")) {
const linuxMatch = /^(.+) on (.+) type ([^,\s)]+) \(/.exec(line);
if (linuxMatch) {
const source = linuxMatch[1];
const mountPoint = linuxMatch[2];
const fsType = linuxMatch[3];
if (source && mountPoint && fsType) entries.push({
source,
mountPoint,
fsType
});
continue;
}
const bsdMatch = /^(.+) on (.+) \(([^,\s)]+)/.exec(line);
if (bsdMatch) {
const source = bsdMatch[1];
const mountPoint = bsdMatch[2];
const fsType = bsdMatch[3];
if (source && mountPoint && fsType) entries.push({
source,
mountPoint,
fsType
});
}
}
return entries;
}
function isMountCommandTimeout(error) {
return error !== null && typeof error === "object" && "code" in error && error.code === "ETIMEDOUT";
}
function readMountEntries() {
try {
return {
ok: true,
value: parseProcMountInfoEntries(fs.readFileSync(PROC_MOUNTINFO_PATH, "utf8"))
};
} catch {}
try {
return {
ok: true,
value: parseMountCommandEntries(String(process.getBuiltinModule("node:child_process").execFileSync("mount", [], {
killSignal: "SIGKILL",
timeout: MOUNT_COMMAND_TIMEOUT_MS
})))
};
} catch (error) {
return isMountCommandTimeout(error) ? {
ok: false,
error: "timeout"
} : {
ok: true,
value: []
};
}
}
function isPathWithinMount(targetPath, mountPoint) {
const resolvedTarget = path.resolve(targetPath);
const resolvedMountPoint = path.resolve(mountPoint);
return resolvedTarget === resolvedMountPoint || resolvedMountPoint === path.parse(resolvedMountPoint).root || resolvedTarget.startsWith(`${resolvedMountPoint}${path.sep}`);
}
function isSshfsMountSource(source) {
if (!source) return false;
const normalized = source.toLowerCase();
return normalized === "sshfs" || normalized.startsWith("sshfs#") || normalized.startsWith("sshfs@") || /^(?:[^/\s:]+@)?[^/\s:]+:.*/u.test(source);
}
function resolveMountTypeJournalPolicy(entry) {
const normalized = entry.fsType.toLowerCase();
if (normalized.startsWith("nfs") || NETWORK_FILESYSTEM_TYPES.has(normalized)) return "rollback";
if (CROSS_VM_FILESYSTEM_TYPES.has(normalized) || normalized.startsWith("9p")) return "rollback";
if (normalized === "fuse.sshfs") return "unsupported";
if ((normalized === "macfuse" || normalized === "osxfuse") && isSshfsMountSource(entry.source)) return "unsupported";
return "wal";
}
function resolveMountEntryJournalPolicy(targetPath, mountEntries) {
const mountEntry = mountEntries.filter((entry) => isPathWithinMount(targetPath, entry.mountPoint)).toSorted((a, b) => b.mountPoint.length - a.mountPoint.length)[0];
return mountEntry ? resolveMountTypeJournalPolicy(mountEntry) : "wal";
}
function combineMountEntryJournalPolicies(targetPaths) {
const mountResult = readMountEntries();
if (!mountResult.ok) return "rollback";
const policies = new Set(targetPaths.map((targetPath) => resolveMountEntryJournalPolicy(targetPath, mountResult.value)));
if (policies.has("unsupported")) return "unsupported";
return policies.has("rollback") ? "rollback" : "wal";
}
function isWindowsUncPath(targetPath) {
return /^\\\\\?\\UNC\\[^\\]+\\[^\\]+/i.test(targetPath) || /^\\\\(?![?.]\\)[^\\]+\\[^\\]+/.test(targetPath);
}
function isWindowsDrivePath(targetPath) {
return /^[A-Za-z]:[\\/]/.test(targetPath) || /^\\\\\?\\[A-Za-z]:[\\/]/i.test(targetPath);
}
function resolvePathJournalPolicy(targetPath) {
if (process.platform === "win32") {
const normalizedTargetPath = path.win32.normalize(targetPath);
if (isWindowsUncPath(normalizedTargetPath)) return "rollback";
if (isWindowsDrivePath(normalizedTargetPath)) try {
return isWindowsUncPath(path.win32.normalize(fs.realpathSync.native(targetPath))) ? "rollback" : "wal";
} catch {
return "rollback";
}
}
const checkedPaths = findExistingVolumePaths(targetPath);
if (!checkedPaths) return "wal";
const mountLookupPaths = [checkedPaths.originalPath, checkedPaths.canonicalPath];
if (typeof fs.statfsSync !== "function") return combineMountEntryJournalPolicies(mountLookupPaths);
try {
const filesystemType = fs.statfsSync(checkedPaths.canonicalPath).type;
if (filesystemType === LINUX_NFS_SUPER_MAGIC || filesystemType === LINUX_SMB_SUPER_MAGIC || filesystemType === LINUX_CIFS_SUPER_MAGIC || filesystemType === LINUX_SMB2_SUPER_MAGIC || filesystemType === LINUX_V9FS_SUPER_MAGIC) return "rollback";
} catch {
return combineMountEntryJournalPolicies(mountLookupPaths);
}
return combineMountEntryJournalPolicies(mountLookupPaths);
}
function readJournalModeResult(row) {
if (!row || typeof row !== "object") return null;
const record = row;
const value = record.journal_mode ?? Object.values(record)[0];
return typeof value === "string" ? value.toLowerCase() : null;
}
function hasInMemoryMainDatabase(db) {
return db.prepare("PRAGMA database_list;").all().find((row) => row.name === "main")?.file === "";
}
function readCheckpointBusyResult(row) {
if (!row || typeof row !== "object") return false;
const record = row;
const value = record.busy ?? Object.values(record)[0];
return value === 1 || value === 1n;
}
function statSqliteSidecarTarget(pathname) {
try {
return fs.statSync(pathname, { bigint: true });
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return;
throw error;
}
}
function isSqliteWalSidecarSplitBrain(descriptor, target) {
return descriptor.nlink === 0n || !target || descriptor.dev !== target.dev || descriptor.ino !== target.ino;
}
function detectSqliteWalSplitBrain(databasePath) {
let descriptors;
try {
descriptors = fs.readdirSync(PROC_SELF_FD_PATH);
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return;
throw error;
}
const sidecarPaths = [`${databasePath}-wal`, `${databasePath}-shm`];
for (const descriptorName of descriptors) {
const descriptorPath = path.join(PROC_SELF_FD_PATH, descriptorName);
let linkedPath;
try {
linkedPath = fs.readlinkSync(descriptorPath);
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) continue;
throw error;
}
const sidecarPath = sidecarPaths.find((candidate) => linkedPath === candidate || linkedPath === `${candidate} (deleted)`);
if (!sidecarPath) continue;
let descriptor;
try {
descriptor = fs.fstatSync(Number(descriptorName), { bigint: true });
} catch (error) {
if (hasErrnoCode(error, "EBADF") || hasErrnoCode(error, "ENOENT")) continue;
throw error;
}
try {
if (fs.readlinkSync(descriptorPath) !== linkedPath) continue;
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) continue;
throw error;
}
const target = statSqliteSidecarTarget(sidecarPath);
if (!isSqliteWalSidecarSplitBrain(descriptor, target)) continue;
return {
event: "sqlite_wal_sidecar_identity_mismatch",
databasePath,
descriptorDevice: descriptor.dev.toString(),
descriptorInode: descriptor.ino.toString(),
sidecarPath,
...target ? {
targetDevice: target.dev.toString(),
targetInode: target.ino.toString()
} : {}
};
}
}
function terminateForSqliteWalSplitBrain(splitBrain, databaseLabel) {
try {
fs.writeSync(process.stderr.fd, `${JSON.stringify({
level: "fatal",
subsystem: "infra/sqlite-wal",
message: SQLITE_WAL_SPLIT_BRAIN_FATAL_MESSAGE,
...splitBrain,
databaseLabel,
pid: process.pid
})}\n`);
} catch {}
try {
process.kill(process.pid, "SIGKILL");
} finally {
process.abort();
}
}
function requireRollbackJournalMode(db, options) {
const journalMode = readJournalModeResult(db.prepare("PRAGMA journal_mode = DELETE;").get());
if (journalMode !== "delete") {
const label = options.databaseLabel ?? "sqlite database";
const location = options.databasePath ? ` at ${options.databasePath}` : "";
throw new Error(`${label}${location} is on a network-backed volume but SQLite kept journal_mode=${journalMode ?? "unknown"}; refusing to continue with WAL on network storage.`);
}
}
function enableWalJournalMode(db, retryTimeoutMs, options) {
const deadline = performance.now() + retryTimeoutMs;
let restoreBusyTimeout = false;
try {
while (true) try {
db.exec("PRAGMA journal_mode = WAL;");
const journalMode = readJournalModeResult(db.prepare("PRAGMA journal_mode;").get());
if (journalMode === "wal") return true;
if (journalMode === "memory" && hasInMemoryMainDatabase(db)) return false;
const label = options.databaseLabel ?? "sqlite database";
const location = options.databasePath ? ` at ${options.databasePath}` : "";
throw new Error(`${label}${location} could not enable WAL; SQLite kept journal_mode=${journalMode ?? "unknown"}.`);
} catch (error) {
const remainingMs = Math.max(0, deadline - performance.now());
if (!isSqliteLockError(error) || remainingMs <= 0) throw error;
if (!restoreBusyTimeout) {
configureSqliteBusyTimeout(db, 0);
restoreBusyTimeout = true;
}
Atomics.wait(JOURNAL_MODE_RETRY_SLEEP, 0, 0, Math.min(JOURNAL_MODE_RETRY_INTERVAL_MS, remainingMs));
}
} finally {
if (restoreBusyTimeout) configureSqliteBusyTimeout(db, retryTimeoutMs);
}
}
function enableMacosCheckpointFullfsync(db) {
if (process.platform !== "darwin") return;
try {
db.exec("PRAGMA checkpoint_fullfsync = 1;");
} catch {}
}
function refuseUnsupportedFilesystem(options) {
const label = options.databaseLabel ?? "sqlite database";
const location = options.databasePath ? ` at ${options.databasePath}` : "";
throw new Error(`${label}${location} is on SSHFS, which cannot safely coordinate SQLite writes across mounts; refusing to open the database.`);
}
/** Configure safe journaling pragmas and return a handle for checkpoint/close maintenance. */
function configureSqliteWalMaintenance(db, options = {}) {
const busyTimeoutMs = options.busyTimeoutMs === void 0 ? 0 : configureSqliteBusyTimeout(db, options.busyTimeoutMs);
const autoCheckpointPages = normalizeSqliteNonNegativeInteger(options.autoCheckpointPages ?? DEFAULT_SQLITE_WAL_AUTOCHECKPOINT_PAGES, "autoCheckpointPages");
const checkpointIntervalMs = normalizeSqliteNonNegativeInteger(options.checkpointIntervalMs ?? DEFAULT_SQLITE_WAL_CHECKPOINT_INTERVAL_MS, "checkpointIntervalMs");
const timerIntervalMs = Math.min(checkpointIntervalMs, MAX_TIMER_TIMEOUT_MS);
const checkpointMode = options.checkpointMode ?? "TRUNCATE";
const periodicCheckpointMode = options.checkpointMode ?? "PASSIVE";
const journalPolicy = options.databasePath ? resolvePathJournalPolicy(options.databasePath) : "wal";
if (journalPolicy === "unsupported") refuseUnsupportedFilesystem(options);
if (journalPolicy === "rollback") {
requireRollbackJournalMode(db, options);
return {
checkpoint: () => true,
close: () => true
};
}
if (!enableWalJournalMode(db, busyTimeoutMs, options)) return {
checkpoint: () => true,
close: () => true
};
enableMacosCheckpointFullfsync(db);
db.exec(`PRAGMA wal_autocheckpoint = ${autoCheckpointPages};`);
db.exec(`PRAGMA journal_size_limit = ${DEFAULT_SQLITE_WAL_JOURNAL_SIZE_LIMIT_BYTES};`);
const tripwireDatabasePath = process.platform === "linux" && options.databasePath && fs.existsSync(options.databasePath) ? fs.realpathSync.native(options.databasePath) : void 0;
let invalidated = false;
let splitBrainDetectionEnabled = Boolean(tripwireDatabasePath);
let splitBrainDetectionWarningLogged = false;
const runCheckpoint = (mode) => {
try {
if (readCheckpointBusyResult(db.prepare(`PRAGMA wal_checkpoint(${mode});`).get())) {
const label = options.databaseLabel ?? "sqlite database";
const error = /* @__PURE__ */ new Error(`${label} WAL checkpoint ${mode} remained busy`);
options.onCheckpointError?.(error);
return false;
}
return true;
} catch (error) {
options.onCheckpointError?.(error);
return false;
}
};
const runIncrementalVacuum = () => {
try {
db.exec(`PRAGMA incremental_vacuum(${INCREMENTAL_VACUUM_MAX_PAGES_PER_PASS});`);
} catch (error) {
options.onCheckpointError?.(error);
}
};
const checkpoint = () => !invalidated && runCheckpoint(checkpointMode);
let timer = null;
if (timerIntervalMs > 0) {
timer = runInSqliteMaintenanceContext(() => setInterval(() => {
if (tripwireDatabasePath && splitBrainDetectionEnabled) {
let splitBrain;
try {
splitBrain = detectSqliteWalSplitBrain(tripwireDatabasePath);
} catch (error) {
splitBrainDetectionEnabled = false;
if (!splitBrainDetectionWarningLogged) {
splitBrainDetectionWarningLogged = true;
log.warn("SQLite WAL split-brain detection disabled", {
databaseLabel: options.databaseLabel,
databasePath: tripwireDatabasePath,
error: error instanceof Error ? error.message : String(error)
});
}
}
if (splitBrain) {
invalidated = true;
if (timer) {
clearInterval(timer);
timer = null;
}
terminateForSqliteWalSplitBrain(splitBrain, options.databaseLabel);
}
}
runCheckpoint(periodicCheckpointMode);
runIncrementalVacuum();
}, timerIntervalMs));
timer.unref?.();
}
return {
checkpoint,
close: (closeOptions) => {
if (timer) {
clearInterval(timer);
timer = null;
}
if (invalidated) return false;
return runCheckpoint(closeOptions?.checkpointMode ?? checkpointMode);
}
};
}
/**
* Register a best-effort exit-time close for a SQLite handle cache. Returns an
* unregister callback the cache's orderly close path must invoke, so tests and
* runtime shutdowns do not accumulate listeners on shared worker processes.
*/
function registerSqliteCacheExitClose(closeAll) {
const closeOnExit = () => {
try {
closeAll();
} catch {}
};
process.once("exit", closeOnExit);
return () => {
process.removeListener("exit", closeOnExit);
};
}
/** Configure per-connection SQLite pragmas in the safe lock-retry/WAL order. */
function configureSqliteConnectionPragmas(db, options = {}) {
const { foreignKeys, synchronous, ...walOptions } = options;
const maintenance = configureSqliteWalMaintenance(db, walOptions);
if (synchronous) db.exec(`PRAGMA synchronous = ${synchronous};`);
if (foreignKeys) db.exec("PRAGMA foreign_keys = ON;");
return maintenance;
}
//#endregion
export { registerSqliteCacheExitClose as i, configureSqlitePreSchemaPragmas as n, configureSqliteWalMaintenance as r, configureSqliteConnectionPragmas as t };