| author | |
| committer | |
| log | a74ce48f9630e3e672f687c1cb3b38f2187ecf2d |
| tree | 799a552cd50b0a0a7f5ef41cee138a804801e30b |
| parent | aef0e9ca401acb40057a286a19de8768809db960 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>2 files changed, 21 insertions(+), 0 deletions(-)
apps/server/src/persistence/Sqlite.test.ts+15| ... | @@ -114,6 +114,21 @@ it.effect("collects planner statistics for an existing database on open", () => | ... | @@ -114,6 +114,21 @@ it.effect("collects planner statistics for an existing database on open", () => |
| 114 | ); | 114 | ); |
| 115 | }); | 115 | }); |
| 116 | 116 | ||
| 117 | it.effect("only fsyncs the database file at WAL checkpoints", () => { | ||
| 118 | const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-sqlite-sync-")); | ||
| 119 | const dbPath = NodePath.join(tempDir, "state.sqlite"); | ||
| 120 | |||
| 121 | return Effect.gen(function* () { | ||
| 122 | const sql = yield* SqlClient.SqlClient; | ||
| 123 | const rows = yield* sql<{ readonly synchronous: number }>`PRAGMA synchronous`; | ||
| 124 | // 1 is NORMAL. | ||
| 125 | assert.equal(rows[0]?.synchronous, 1); | ||
| 126 | }).pipe( | ||
| 127 | Effect.provide(SqlitePersistence.layerFromPath(dbPath).pipe(Layer.provide(NodeServices.layer))), | ||
| 128 | Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))), | ||
| 129 | ); | ||
| 130 | }); | ||
| 131 | |||
| 117 | it.effect("applies busy_timeout in the shared persistence setup", () => | 132 | it.effect("applies busy_timeout in the shared persistence setup", () => |
| 118 | Effect.gen(function* () { | 133 | Effect.gen(function* () { |
| 119 | const sql = yield* SqlClient.SqlClient; | 134 | const sql = yield* SqlClient.SqlClient; |
apps/server/src/persistence/Sqlite.ts+6| ... | @@ -19,6 +19,12 @@ const layerSetup = Layer.effectDiscard( | ... | @@ -19,6 +19,12 @@ const layerSetup = Layer.effectDiscard( |
| 19 | yield* sql`PRAGMA busy_timeout = 5000;`; | 19 | yield* sql`PRAGMA busy_timeout = 5000;`; |
| 20 | yield* sql`PRAGMA foreign_keys = ON;`; | 20 | yield* sql`PRAGMA foreign_keys = ON;`; |
| 21 | yield* sql`PRAGMA journal_mode = WAL;`; | 21 | yield* sql`PRAGMA journal_mode = WAL;`; |
| 22 | // FULL fsyncs the WAL on every commit, on the main thread. Every provider | ||
| 23 | // event is its own commit, so a slow or busy disk stalls the event loop | ||
| 24 | // and clients drop. In WAL mode NORMAL only fsyncs at checkpoints. It | ||
| 25 | // cannot corrupt the database and an app crash loses nothing; an OS crash | ||
| 26 | // or power loss can roll back commits since the last checkpoint. | ||
| 27 | yield* sql`PRAGMA synchronous = NORMAL;`; | ||
| 22 | // PASSIVE checkpoints never shrink the -wal file, so it otherwise keeps its | 28 | // PASSIVE checkpoints never shrink the -wal file, so it otherwise keeps its |
| 23 | // largest size until the last connection closes. | 29 | // largest size until the last connection closes. |
| 24 | yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`); | 30 | yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`); |