diff --git a/apps/server/scripts/t3-sqlite-state.test.ts b/apps/server/scripts/t3-sqlite-state.test.ts index fa5471bfa8c4..27a2dcc593df 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 5e274588671f..a5dbb19311fa 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/Layers/Sqlite.ts and can checkpoint + // state.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/Layers/Sqlite.test.ts b/apps/server/src/persistence/Layers/Sqlite.test.ts index 5bcbd35e918c..e1832cfbe952 100644 --- a/apps/server/src/persistence/Layers/Sqlite.test.ts +++ b/apps/server/src/persistence/Layers/Sqlite.test.ts @@ -94,3 +94,13 @@ it.effect("applies busy_timeout in the shared persistence setup", () => assert.equal(rows[0]?.timeout, 5000); }).pipe(Effect.provide(SqlitePersistenceMemory)), ); + +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(SqlitePersistenceMemory)), +); diff --git a/apps/server/src/persistence/Layers/Sqlite.ts b/apps/server/src/persistence/Layers/Sqlite.ts index 56536087d69c..cc6b81658803 100644 --- a/apps/server/src/persistence/Layers/Sqlite.ts +++ b/apps/server/src/persistence/Layers/Sqlite.ts @@ -18,6 +18,9 @@ const setup = Layer.effectDiscard( yield* sql`PRAGMA busy_timeout = 5000;`; yield* sql`PRAGMA foreign_keys = ON;`; yield* sql`PRAGMA journal_mode = WAL;`; + // macOS fsync() does not flush the drive cache. Checkpoint syncs must use + // F_FULLFSYNC so a power loss cannot publish page references without the pages. + 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};`);