Skip to content

Commit aa10f1d

Browse files
committed
Harden macOS SQLite WAL checkpoints
1 parent 5bcd25f commit aa10f1d

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

src/infra/sqlite-wal.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ describe("sqlite WAL maintenance", () => {
4141

4242
it("enables WAL mode and explicit autocheckpointing", () => {
4343
const db = createMockDb();
44+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
4445

4546
configureSqliteWalMaintenance(db, { checkpointIntervalMs: 0 });
4647

@@ -51,10 +52,44 @@ describe("sqlite WAL maintenance", () => {
5152
);
5253
});
5354

55+
it("enables fullfsync barriers for WAL checkpoints on macOS", () => {
56+
const db = createMockDb();
57+
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
58+
59+
configureSqliteWalMaintenance(db, { checkpointIntervalMs: 0 });
60+
61+
expect(db["exec"]).toHaveBeenNthCalledWith(1, "PRAGMA journal_mode = WAL;");
62+
expect(db["exec"]).toHaveBeenNthCalledWith(2, "PRAGMA checkpoint_fullfsync = 1;");
63+
expect(db["exec"]).toHaveBeenNthCalledWith(
64+
3,
65+
`PRAGMA wal_autocheckpoint = ${DEFAULT_SQLITE_WAL_AUTOCHECKPOINT_PAGES};`,
66+
);
67+
});
68+
69+
it("continues WAL setup if macOS checkpoint fullfsync is unavailable", () => {
70+
const db = createMockDb();
71+
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
72+
vi.mocked(db["exec"]).mockImplementation((sql) => {
73+
if (sql.includes("checkpoint_fullfsync")) {
74+
throw new Error("unsupported pragma");
75+
}
76+
});
77+
78+
configureSqliteWalMaintenance(db, { checkpointIntervalMs: 0 });
79+
80+
expect(db["exec"]).toHaveBeenNthCalledWith(1, "PRAGMA journal_mode = WAL;");
81+
expect(db["exec"]).toHaveBeenNthCalledWith(2, "PRAGMA checkpoint_fullfsync = 1;");
82+
expect(db["exec"]).toHaveBeenNthCalledWith(
83+
3,
84+
`PRAGMA wal_autocheckpoint = ${DEFAULT_SQLITE_WAL_AUTOCHECKPOINT_PAGES};`,
85+
);
86+
});
87+
5488
it("uses rollback journaling for databases on NFS-backed volumes", () => {
5589
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-nfs-"));
5690
try {
5791
const db = createMockDb();
92+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
5893
const statfs = vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0x6969));
5994

6095
const maintenance = configureSqliteWalMaintenance(db, {
@@ -81,6 +116,7 @@ describe("sqlite WAL maintenance", () => {
81116
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-network-"));
82117
try {
83118
const db = createMockDb();
119+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
84120
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(fsType));
85121

86122
configureSqliteWalMaintenance(db, {
@@ -188,6 +224,7 @@ describe("sqlite WAL maintenance", () => {
188224
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-nfs-"));
189225
try {
190226
const db = createMockDb();
227+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
191228
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
192229
vi.spyOn(fs, "readFileSync").mockReturnValue(
193230
`42 12 0:41 / ${tempDir} rw,relatime - nfs4 server:/share rw\n`,
@@ -208,6 +245,7 @@ describe("sqlite WAL maintenance", () => {
208245
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-sshfs-"));
209246
try {
210247
const db = createMockDb();
248+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
211249
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
212250
vi.spyOn(fs, "readFileSync").mockReturnValue(
213251
`42 12 0:41 / ${tempDir} rw,relatime - fuse.sshfs user@host:/share rw\n`,
@@ -285,6 +323,7 @@ describe("sqlite WAL maintenance", () => {
285323
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-nfs-"));
286324
try {
287325
const db = createMockDb();
326+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
288327
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
289328
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
290329
throw new Error("no proc mountinfo");
@@ -308,6 +347,7 @@ describe("sqlite WAL maintenance", () => {
308347
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-smb-"));
309348
try {
310349
const db = createMockDb();
350+
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
311351
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
312352
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
313353
throw new Error("no proc mountinfo");
@@ -337,6 +377,7 @@ describe("sqlite WAL maintenance", () => {
337377
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-sshfs-macfuse-"));
338378
try {
339379
const db = createMockDb();
380+
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
340381
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
341382
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
342383
throw new Error("no proc mountinfo");
@@ -362,6 +403,7 @@ describe("sqlite WAL maintenance", () => {
362403
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-macfuse-"));
363404
try {
364405
const db = createMockDb();
406+
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
365407
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
366408
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
367409
throw new Error("no proc mountinfo");
@@ -385,6 +427,7 @@ describe("sqlite WAL maintenance", () => {
385427
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-nfs-"));
386428
try {
387429
const db = createMockDb();
430+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
388431
vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
389432
vi.spyOn(fs, "readFileSync").mockImplementation(() => {
390433
throw new Error("no proc mountinfo");
@@ -407,6 +450,7 @@ describe("sqlite WAL maintenance", () => {
407450
it("runs lightweight periodic PASSIVE checkpoints and TRUNCATE on close", () => {
408451
vi.useFakeTimers();
409452
const db = createMockDb();
453+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
410454

411455
const maintenance = configureSqliteWalMaintenance(db, { checkpointIntervalMs: 100 });
412456
expect(db["exec"]).toHaveBeenCalledTimes(2);
@@ -427,6 +471,7 @@ describe("sqlite WAL maintenance", () => {
427471
vi.useFakeTimers();
428472
const setIntervalSpy = vi.spyOn(globalThis, "setInterval");
429473
const db = createMockDb();
474+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
430475

431476
const maintenance = configureSqliteWalMaintenance(db, {
432477
checkpointIntervalMs: Number.MAX_SAFE_INTEGER,
@@ -439,6 +484,7 @@ describe("sqlite WAL maintenance", () => {
439484
it("honors explicit checkpoint mode overrides for periodic and close checkpoints", () => {
440485
vi.useFakeTimers();
441486
const db = createMockDb();
487+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
442488

443489
const maintenance = configureSqliteWalMaintenance(db, {
444490
checkpointIntervalMs: 100,
@@ -456,6 +502,7 @@ describe("sqlite WAL maintenance", () => {
456502
const db = createMockDb();
457503
const error = new Error("busy");
458504
const onCheckpointError = vi.fn();
505+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
459506
vi.mocked(db["exec"]).mockImplementation((sql) => {
460507
if (sql.includes("wal_checkpoint")) {
461508
throw error;
@@ -473,6 +520,7 @@ describe("sqlite WAL maintenance", () => {
473520

474521
it("configures connection pragmas before WAL maintenance", () => {
475522
const db = createMockDb();
523+
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
476524

477525
configureSqliteConnectionPragmas(db, {
478526
busyTimeoutMs: 30_000,

src/infra/sqlite-wal.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,19 @@ function requireRollbackJournalMode(db: DatabaseSync, options: SqliteWalMaintena
274274
}
275275
}
276276

277+
function enableMacosCheckpointFullfsync(db: DatabaseSync): void {
278+
if (process.platform !== "darwin") {
279+
return;
280+
}
281+
try {
282+
db.exec("PRAGMA checkpoint_fullfsync = 1;");
283+
} catch {
284+
// Older SQLite builds may ignore or reject platform-specific pragmas. WAL
285+
// setup should still proceed because this is a durability upgrade, not a
286+
// prerequisite for opening the store.
287+
}
288+
}
289+
277290
function refuseUnsupportedFilesystem(options: SqliteWalMaintenanceOptions): never {
278291
const label = options.databaseLabel ?? "sqlite database";
279292
const location = options.databasePath ? ` at ${options.databasePath}` : "";
@@ -312,6 +325,7 @@ export function configureSqliteWalMaintenance(
312325
};
313326
}
314327
db.exec("PRAGMA journal_mode = WAL;");
328+
enableMacosCheckpointFullfsync(db);
315329
db.exec(`PRAGMA wal_autocheckpoint = ${autoCheckpointPages};`);
316330

317331
const runCheckpoint = (mode: SqliteWalCheckpointMode): boolean => {

0 commit comments

Comments
 (0)