From 614b432a79b0acae38c27c00b1e4eece223ecfe4 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 14 Oct 2026 01:18:11 -0700 Subject: [PATCH] fix(server): make SQLite checkpoints durable on macOS (#19304) Co-authored-by: Soroush Baraouf Co-authored-by: Claude Opus 5.5 --- apps/server/scripts/t3-sqlite-state.test.ts | 19 +++++++++++++++++++ apps/server/scripts/t3-sqlite-state.ts | 3 +++ apps/server/src/persistence/Sqlite.test.ts | 10 ++++++++++ apps/server/src/persistence/Sqlite.ts | 4 ++++ 4 files changed, 36 insertions(+) diff --git a/apps/server/scripts/t3-sqlite-state.test.ts b/apps/server/scripts/t3-sqlite-state.test.ts index 4946d19856399c0253ea0ca36377306e532810bb..e4a309a5d898b3340969603596bebd3239af2ac2 100644 --- a/apps/server/scripts/t3-sqlite-state.test.ts +++ b/apps/server/scripts/t3-sqlite-state.test.ts @@ -75,6 +75,25 @@ it.layer(NodeServices.layer)("t3-sqlite-state", (it) => { }), ); + it.effect("enables checkpoint_fullfsync on its direct connection", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-state-fsync-" }); + yield* createFixtureDatabase(baseDir); + + const result = yield* runSqliteState({ + operation: "query", + baseDir, + sql: "PRAGMA checkpoint_fullfsync", + }); + + assert.equal(result.operation, "query"); + if (result.operation === "query") { + assert.deepStrictEqual(result.rows, [{ checkpoint_fullfsync: 1 }]); + } + }), + ); + it.effect.skipIf(!symlinksSupported)( "backs up isolated state before writes and refuses the shared home", () => diff --git a/apps/server/scripts/t3-sqlite-state.ts b/apps/server/scripts/t3-sqlite-state.ts index f905ac2588a59a1a08d1bce07d05175ee78462d0..90ae053d43b6504763ab22328cc642706414009e 100644 --- a/apps/server/scripts/t3-sqlite-state.ts +++ b/apps/server/scripts/t3-sqlite-state.ts @@ -201,6 +201,9 @@ export const runSqliteState = Effect.fn("runSqliteState")(function* ( const program = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* sql.unsafe("PRAGMA busy_timeout = 5000").unprepared; + // This connection bypasses persistence/Sqlite.ts and can checkpoint + // statev2.sqlite, so it needs the same F_FULLFSYNC checkpoint barrier. + yield* sql.unsafe("PRAGMA checkpoint_fullfsync = ON").unprepared; if (input.operation === "query") { const rows = yield* sql.unsafe(source).unprepared.pipe( diff --git a/apps/server/src/persistence/Sqlite.test.ts b/apps/server/src/persistence/Sqlite.test.ts index f4056c9d0c2fa918069faa10240346ec8bafa295..75f9fc3a80b2cf9645a84dbda44e4930629c8a3c 100644 --- a/apps/server/src/persistence/Sqlite.test.ts +++ b/apps/server/src/persistence/Sqlite.test.ts @@ -136,3 +136,13 @@ it.effect("applies busy_timeout in the shared persistence setup", () => assert.equal(rows[0]?.timeout, 5000); }).pipe(Effect.provide(SqlitePersistence.layerMemory)), ); + +it.effect("enables checkpoint_fullfsync in the shared persistence setup", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const rows = yield* sql<{ + readonly checkpoint_fullfsync: number; + }>`PRAGMA checkpoint_fullfsync`; + assert.equal(rows[0]?.checkpoint_fullfsync, 1); + }).pipe(Effect.provide(SqlitePersistence.layerMemory)), +); diff --git a/apps/server/src/persistence/Sqlite.ts b/apps/server/src/persistence/Sqlite.ts index d3760db007c10a327587caaf5eb909b32b60609a..8ba6f8437d4f807494bf47e07d6093b33b422b4b 100644 --- a/apps/server/src/persistence/Sqlite.ts +++ b/apps/server/src/persistence/Sqlite.ts @@ -25,6 +25,10 @@ const layerSetup = Layer.effectDiscard( // cannot corrupt the database and an app crash loses nothing; an OS crash // or power loss can roll back commits since the last checkpoint. yield* sql`PRAGMA synchronous = NORMAL;`; + // Checkpoints are now the only sync, and macOS fsync() does not flush the + // drive cache. F_FULLFSYNC makes them reach disk; other platforms ignore it. + // Per-commit fullfsync stays off. + yield* sql`PRAGMA checkpoint_fullfsync = ON;`; // PASSIVE checkpoints never shrink the -wal file, so it otherwise keeps its // largest size until the last connection closes. yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`); -- 2.54.0