mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(sqlite): stop mount probes blocking database startup (#109432)
* fix(sqlite): bound fallback mount classification * test(sqlite): cover mount probe failure policy * test(sqlite): use tracked mount probe fixtures * perf(sqlite): keep mount result import type-only * style(sqlite): format mount probe result * perf(sqlite): defer mount process module loading * perf(sqlite): avoid eager mount parser dependency * refactor(sqlite): isolate lock error detection * revert: keep sqlite lock errors colocated --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -331,21 +331,68 @@ describe("sqlite WAL maintenance", () => {
|
||||
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
|
||||
throw new Error("no proc mountinfo");
|
||||
});
|
||||
vi.spyOn(childProcess, "execFileSync").mockReturnValue(
|
||||
Buffer.from(`server:/share on ${tempDir} (nfs, nodev, nosuid)\n`),
|
||||
);
|
||||
const mount = vi
|
||||
.spyOn(childProcess, "execFileSync")
|
||||
.mockReturnValue(Buffer.from(`server:/share on ${tempDir} (nfs, nodev, nosuid)\n`));
|
||||
|
||||
configureSqliteWalMaintenance(db, {
|
||||
checkpointIntervalMs: 0,
|
||||
databasePath: path.join(tempDir, "openclaw.sqlite"),
|
||||
});
|
||||
|
||||
expect(mount).toHaveBeenCalledWith("mount", [], {
|
||||
killSignal: "SIGKILL",
|
||||
timeout: 1_000,
|
||||
});
|
||||
expect(mount).toHaveBeenCalledTimes(1);
|
||||
expect(db["prepare"]).toHaveBeenCalledWith("PRAGMA journal_mode = DELETE;");
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("uses rollback journaling when mount classification times out", () => {
|
||||
const tempDir = tempDirs.make("openclaw-sqlite-mount-timeout-");
|
||||
const db = createMockDb();
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
|
||||
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
|
||||
throw new Error("no proc mountinfo");
|
||||
});
|
||||
vi.spyOn(childProcess, "execFileSync").mockImplementation(() => {
|
||||
throw Object.assign(new Error("spawnSync mount ETIMEDOUT"), { code: "ETIMEDOUT" });
|
||||
});
|
||||
|
||||
configureSqliteWalMaintenance(db, {
|
||||
checkpointIntervalMs: 0,
|
||||
databasePath: path.join(tempDir, "openclaw.sqlite"),
|
||||
});
|
||||
|
||||
expect(db["prepare"]).toHaveBeenCalledWith("PRAGMA journal_mode = DELETE;");
|
||||
expect(db["exec"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves WAL policy when mount classification fails without timing out", () => {
|
||||
const tempDir = tempDirs.make("openclaw-sqlite-mount-error-");
|
||||
const db = createMockDb();
|
||||
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
||||
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
|
||||
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
|
||||
throw new Error("no proc mountinfo");
|
||||
});
|
||||
vi.spyOn(childProcess, "execFileSync").mockImplementation(() => {
|
||||
throw Object.assign(new Error("spawnSync mount ENOENT"), { code: "ENOENT" });
|
||||
});
|
||||
|
||||
configureSqliteWalMaintenance(db, {
|
||||
checkpointIntervalMs: 0,
|
||||
databasePath: path.join(tempDir, "openclaw.sqlite"),
|
||||
});
|
||||
|
||||
expect(db["exec"]).toHaveBeenNthCalledWith(1, "PRAGMA journal_mode = WAL;");
|
||||
expect(db["prepare"]).toHaveBeenCalledWith("PRAGMA journal_mode;");
|
||||
});
|
||||
|
||||
it("uses macOS SMB mount filesystem names", () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-smb-"));
|
||||
try {
|
||||
|
||||
+44
-19
@@ -1,9 +1,8 @@
|
||||
// Configures SQLite WAL and related pragmas for local stores.
|
||||
import childProcess from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import type { Result } from "@openclaw/normalization-core/result";
|
||||
import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js";
|
||||
import { isSqliteLockError } from "./sqlite-transaction.js";
|
||||
|
||||
@@ -19,6 +18,8 @@ const LINUX_SMB_SUPER_MAGIC = 0x517b;
|
||||
const LINUX_CIFS_SUPER_MAGIC = 0xff534d42;
|
||||
const LINUX_SMB2_SUPER_MAGIC = 0xfe534d42;
|
||||
const PROC_MOUNTINFO_PATH = "/proc/self/mountinfo";
|
||||
// Filesystem classification runs during database open, so never let the fallback probe stall it.
|
||||
const MOUNT_COMMAND_TIMEOUT_MS = 1_000;
|
||||
const NETWORK_FILESYSTEM_TYPES = new Set(["cifs", "smbfs", "smb2", "smb3"]);
|
||||
const JOURNAL_MODE_RETRY_INTERVAL_MS = 10;
|
||||
const JOURNAL_MODE_RETRY_SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
||||
@@ -145,36 +146,57 @@ function parseMountCommandEntries(contents: string): MountEntry[] {
|
||||
for (const line of contents.split("\n")) {
|
||||
const linuxMatch = /^(.+) on (.+) type ([^,\s)]+) \(/.exec(line);
|
||||
if (linuxMatch) {
|
||||
entries.push({
|
||||
source: linuxMatch[1],
|
||||
mountPoint: expectDefined(linuxMatch[2], "linux match capture group 2"),
|
||||
fsType: expectDefined(linuxMatch[3], "linux match capture group 3"),
|
||||
});
|
||||
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) {
|
||||
entries.push({
|
||||
source: bsdMatch[1],
|
||||
mountPoint: expectDefined(bsdMatch[2], "bsd match capture group 2"),
|
||||
fsType: expectDefined(bsdMatch[3], "bsd match capture group 3"),
|
||||
});
|
||||
const source = bsdMatch[1];
|
||||
const mountPoint = bsdMatch[2];
|
||||
const fsType = bsdMatch[3];
|
||||
if (source && mountPoint && fsType) {
|
||||
entries.push({ source, mountPoint, fsType });
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function readMountEntries(): MountEntry[] {
|
||||
function isMountCommandTimeout(error: unknown): boolean {
|
||||
return (
|
||||
error !== null && typeof error === "object" && "code" in error && error.code === "ETIMEDOUT"
|
||||
);
|
||||
}
|
||||
|
||||
function readMountEntries(): Result<MountEntry[], "timeout"> {
|
||||
try {
|
||||
return parseProcMountInfoEntries(fs.readFileSync(PROC_MOUNTINFO_PATH, "utf8"));
|
||||
return {
|
||||
ok: true,
|
||||
value: parseProcMountInfoEntries(fs.readFileSync(PROC_MOUNTINFO_PATH, "utf8")),
|
||||
};
|
||||
} catch {
|
||||
// macOS/BSD expose filesystem type names in `mount` output instead of
|
||||
// Linux superblock magic, so keep this fallback for named filesystem types.
|
||||
}
|
||||
try {
|
||||
return parseMountCommandEntries(String(childProcess.execFileSync("mount", [])));
|
||||
} catch {
|
||||
return [];
|
||||
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: [] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,9 +250,12 @@ function resolveMountEntryJournalPolicy(
|
||||
function combineMountEntryJournalPolicies(
|
||||
targetPaths: readonly string[],
|
||||
): SqliteFilesystemJournalPolicy {
|
||||
const mountEntries = readMountEntries();
|
||||
const mountResult = readMountEntries();
|
||||
if (!mountResult.ok) {
|
||||
return "rollback";
|
||||
}
|
||||
const policies = new Set(
|
||||
targetPaths.map((targetPath) => resolveMountEntryJournalPolicy(targetPath, mountEntries)),
|
||||
targetPaths.map((targetPath) => resolveMountEntryJournalPolicy(targetPath, mountResult.value)),
|
||||
);
|
||||
if (policies.has("unsupported")) {
|
||||
return "unsupported";
|
||||
|
||||
Reference in New Issue
Block a user