UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

461 lines (460 loc) 17.9 kB
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 };