Skip to content

Commit 4a3a9b7

Browse files
committed
fix(store): bound the WAL with journal_size_limit so a starved log is reclaimed
On-disk write connections set no journal_size_limit, so the -wal file is never physically shrunk: all our checkpoints are PASSIVE (TRUNCATE is deliberately avoided because its ftruncate(fd,0) can SIGBUS a sibling mmap'd process on macOS), and PASSIVE never ftruncates. Under checkpoint starvation the WAL then grows without bound (reported: 115 GB, filled the drive) and even a later successful checkpoint leaves the file at its peak size (DeusData#1083). Set journal_size_limit = 256 MiB on write connections so SQLite truncates the -wal back to that bound whenever a checkpoint can finally reset the log — reclaiming disk opportunistically during a session, not only on clean close. 256 MiB is far above the healthy WAL (~4 MiB under the default 1000-page autocheckpoint), so normal indexing never triggers truncate/regrow churn; the limit only fires after abnormal growth. No TRUNCATE checkpoint is introduced. Scope: this is the *reclaim* half of DeusData#1083. It does not prevent the peak under continuous reader contention (a checkpoint can't reset the log while a reader holds a mark) — the write-amplification driver (concurrent full re-indexes of one repo) is serialized by the cross-process pipeline lock in DeusData#892. Test: a file-backed write store now reports journal_size_limit = 256 MiB (RED on main: -1 / unlimited). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent a688d85 commit 4a3a9b7

3 files changed

Lines changed: 65 additions & 0 deletions

File tree

‎src/store/store.c‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,20 @@ static int configure_pragmas(cbm_store_t *s, bool in_memory, bool read_only) {
432432
if (rc != CBM_STORE_OK) {
433433
return rc;
434434
}
435+
/* #1083: bound the WAL file so a checkpoint-starved log is physically
436+
* reclaimed the next time a checkpoint can reset it. Our checkpoints are
437+
* all PASSIVE (they never ftruncate — see cbm_store_checkpoint's SIGBUS
438+
* note), so without a size limit the -wal file only ever grows; a
439+
* journal_size_limit truncates it back to N bytes on the next successful
440+
* reset. N is far above the healthy WAL (~4 MiB under the default
441+
* 1000-page autocheckpoint), so normal indexing never triggers
442+
* truncate/regrow churn — it only fires after abnormal growth. We do NOT
443+
* use a TRUNCATE checkpoint: its ftruncate(fd,0) can raise SIGBUS in a
444+
* sibling process that has the DB mmap'd on macOS. */
445+
rc = exec_sql(s, "PRAGMA journal_size_limit = 268435456;"); /* 256 MiB */
446+
if (rc != CBM_STORE_OK) {
447+
return rc;
448+
}
435449
char mmap_sql[ST_BUF_64];
436450
snprintf(mmap_sql, sizeof(mmap_sql), "PRAGMA mmap_size = %lld;",
437451
(long long)cbm_store_resolve_mmap_size());
@@ -1081,6 +1095,25 @@ int cbm_store_checkpoint(cbm_store_t *s) {
10811095
return exec_sql(s, "PRAGMA optimize;");
10821096
}
10831097

1098+
/* #1083: the WAL size limit configured on this (write) connection, in bytes.
1099+
* -1 means unlimited (SQLite's default — the pre-fix behavior). Per-connection
1100+
* and not persisted, so it can only be read on the connection that set it. */
1101+
int64_t cbm_store_journal_size_limit(cbm_store_t *s) {
1102+
if (!s || !s->db) {
1103+
return -1;
1104+
}
1105+
sqlite3_stmt *stmt = NULL;
1106+
if (sqlite3_prepare_v2(s->db, "PRAGMA journal_size_limit;", -1, &stmt, NULL) != SQLITE_OK) {
1107+
return -1;
1108+
}
1109+
int64_t limit = -1;
1110+
if (sqlite3_step(stmt) == SQLITE_ROW) {
1111+
limit = sqlite3_column_int64(stmt, 0);
1112+
}
1113+
sqlite3_finalize(stmt);
1114+
return limit;
1115+
}
1116+
10841117
/* ── Dump ───────────────────────────────────────────────────────── */
10851118

10861119
/* Dump entire in-memory database to a file via sqlite3_backup.

‎src/store/store.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ int cbm_store_create_indexes(cbm_store_t *s);
262262
/* Force WAL checkpoint + PRAGMA optimize. */
263263
int cbm_store_checkpoint(cbm_store_t *s);
264264

265+
/* #1083: the WAL size limit (journal_size_limit) applied to this write
266+
* connection, in bytes; -1 = unlimited (SQLite default / pre-fix). */
267+
int64_t cbm_store_journal_size_limit(cbm_store_t *s);
268+
265269
/* Resolve the mmap_size pragma value applied to on-disk stores from the
266270
* CBM_SQLITE_MMAP_SIZE environment variable. Defaults to 67108864 (64 MB)
267271
* when the variable is unset, malformed, or partially numeric. Negative

‎tests/test_store_pragmas.c‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,33 @@ TEST(store_open_with_mmap_disabled) {
8686
PASS();
8787
}
8888

89+
/* #1083: on-disk write connections must bound the WAL via journal_size_limit
90+
* so a checkpoint-starved log is physically reclaimed once a checkpoint can
91+
* reset it. On main this is UNSET (-1 = unlimited), so the -wal file only ever
92+
* grows (all our checkpoints are PASSIVE and never ftruncate). Read the pragma
93+
* back on the SAME connection — it's per-connection and not persisted. */
94+
TEST(journal_size_limit_bounds_wal_issue1083) {
95+
char tmp_path[256];
96+
snprintf(tmp_path, sizeof(tmp_path), "%s/cbm_test_jsl_%d.db", cbm_tmpdir(), (int)getpid());
97+
unlink(tmp_path);
98+
99+
cbm_store_t *s = cbm_store_open_path(tmp_path);
100+
ASSERT(s != NULL);
101+
/* 256 MiB — far above the healthy WAL (~4 MiB), so no truncate/regrow churn
102+
* in normal operation; it only fires after abnormal (starved) growth. */
103+
ASSERT(cbm_store_journal_size_limit(s) == (int64_t)268435456);
104+
cbm_store_close(s);
105+
106+
unlink(tmp_path);
107+
char tmp_wal[300];
108+
char tmp_shm[300];
109+
snprintf(tmp_wal, sizeof(tmp_wal), "%s-wal", tmp_path);
110+
snprintf(tmp_shm, sizeof(tmp_shm), "%s-shm", tmp_path);
111+
unlink(tmp_wal);
112+
unlink(tmp_shm);
113+
PASS();
114+
}
115+
89116

90117
/* #896: a row-scan that dies mid-stream (SQLITE_CORRUPT) must surface a
91118
* loud store error, not masquerade as a clean end of results. Counts are
@@ -179,6 +206,7 @@ TEST(corrupt_page_scan_returns_error_not_truncation) {
179206
}
180207

181208
SUITE(store_pragmas) {
209+
RUN_TEST(journal_size_limit_bounds_wal_issue1083);
182210
RUN_TEST(corrupt_page_scan_returns_error_not_truncation);
183211
RUN_TEST(mmap_size_default_when_unset);
184212
RUN_TEST(mmap_size_zero_disables_mmap);

0 commit comments

Comments
 (0)