Skip to content

Commit 2215356

Browse files
edwardmhughesDeusData
authored andcommitted
fix(store): use PASSIVE checkpoint to avoid file-shrink under concurrent readers
cbm_store_checkpoint() invoked SQLITE_CHECKPOINT_TRUNCATE, the most aggressive mode. When two cbm-mcp processes share a cache dir, one process's TRUNCATE can shrink files while another has them mmap'd, raising SIGBUS on macOS. PASSIVE never blocks readers and never ftruncate()s either file; SQLite still autocheckpoints in PASSIVE mode at 1000-page boundaries, so reclamation is unaffected for single-process users. Recommended by SQLite docs for shared databases: https://www.sqlite.org/pragma.html#pragma_wal_checkpoint
1 parent 093707c commit 2215356

4 files changed

Lines changed: 90 additions & 2 deletions

File tree

Makefile.cbm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ TEST_STORE_SRCS = \
282282
tests/test_store_search.c \
283283
tests/test_store_arch.c \
284284
tests/test_store_bulk.c \
285-
tests/test_store_pragmas.c
285+
tests/test_store_pragmas.c \
286+
tests/test_store_checkpoint.c
286287

287288
TEST_CYPHER_SRCS = \
288289
tests/test_cypher.c

src/store/store.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,13 @@ int cbm_store_create_indexes(cbm_store_t *s) {
830830
/* ── Checkpoint ─────────────────────────────────────────────────── */
831831

832832
int cbm_store_checkpoint(cbm_store_t *s) {
833-
int rc = sqlite3_wal_checkpoint_v2(s->db, NULL, SQLITE_CHECKPOINT_TRUNCATE, NULL, NULL);
833+
/* PASSIVE never blocks readers and never ftruncate()s either file.
834+
* SQLite recommends PASSIVE for shared databases — TRUNCATE shrinks
835+
* the WAL via ftruncate(fd, 0) on success, which on macOS can raise
836+
* SIGBUS in a sibling process that has the DB mmap'd through SQLite
837+
* when it next faults a page in the now-shorter region.
838+
* See https://www.sqlite.org/c3ref/c_checkpoint_full.html */
839+
int rc = sqlite3_wal_checkpoint_v2(s->db, NULL, SQLITE_CHECKPOINT_PASSIVE, NULL, NULL);
834840
if (rc != SQLITE_OK) {
835841
store_set_error_sqlite(s, "checkpoint");
836842
return CBM_STORE_ERR;

tests/test_main.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ extern void suite_ts_lsp(void);
5353
extern void suite_store_arch(void);
5454
extern void suite_store_bulk(void);
5555
extern void suite_store_pragmas(void);
56+
extern void suite_store_checkpoint(void);
5657
extern void suite_traces(void);
5758
extern void suite_configlink(void);
5859
extern void suite_infrascan(void);
@@ -91,6 +92,7 @@ int main(void) {
9192
RUN_SUITE(store_search);
9293
RUN_SUITE(store_bulk);
9394
RUN_SUITE(store_pragmas);
95+
RUN_SUITE(store_checkpoint);
9496

9597
/* Cypher (M6) */
9698
RUN_SUITE(cypher);

tests/test_store_checkpoint.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* test_store_checkpoint.c — Tests for WAL checkpoint behavior.
3+
*
4+
* Verifies that cbm_store_checkpoint() does not truncate the on-disk
5+
* WAL file. SQLITE_CHECKPOINT_TRUNCATE shrinks the WAL via ftruncate(fd, 0)
6+
* on success; on macOS this can raise SIGBUS in a sibling process that
7+
* has the DB mmap'd through SQLite when it next faults a page in the
8+
* now-shorter region. SQLITE_CHECKPOINT_PASSIVE marks frames as
9+
* checkpointed in the WAL header without changing the file size — disk
10+
* space is reclaimed on the next write cycle, not on every checkpoint.
11+
*/
12+
#include "../src/foundation/compat.h"
13+
#include "test_framework.h"
14+
#include "test_helpers.h"
15+
#include <store/store.h>
16+
#include <stdio.h>
17+
#include <stdlib.h>
18+
#include <string.h>
19+
#include <sys/stat.h>
20+
#include <unistd.h>
21+
22+
TEST(checkpoint_does_not_truncate_wal) {
23+
enum { N_ROWS = 100, PATH_BUF = 256, PATH_BUF_EXT = 300 };
24+
char db_path[PATH_BUF];
25+
snprintf(db_path, sizeof(db_path), "/tmp/cbm_test_ckpt_%d.db", (int)getpid());
26+
char wal_path[PATH_BUF_EXT];
27+
snprintf(wal_path, sizeof(wal_path), "%s-wal", db_path);
28+
char shm_path[PATH_BUF_EXT];
29+
snprintf(shm_path, sizeof(shm_path), "%s-shm", db_path);
30+
unlink(db_path);
31+
unlink(wal_path);
32+
unlink(shm_path);
33+
34+
cbm_store_t *s = cbm_store_open_path(db_path);
35+
ASSERT(s != NULL);
36+
37+
/* Grow WAL beyond zero bytes via direct SQL. */
38+
int rc_sql = cbm_store_exec(
39+
s,
40+
"INSERT OR IGNORE INTO projects(name, indexed_at, root_path) "
41+
"VALUES('p', '2026-01-01', '/tmp/p');");
42+
ASSERT_EQ(rc_sql, 0);
43+
for (int i = 0; i < N_ROWS; i++) {
44+
char sql[256];
45+
snprintf(sql, sizeof(sql),
46+
"INSERT INTO nodes(project, label, name, qualified_name, file_path) "
47+
"VALUES('p', 'Function', 'fn', 'p.module.fn_%d', 'f.c');",
48+
i);
49+
rc_sql = cbm_store_exec(s, sql);
50+
ASSERT_EQ(rc_sql, 0);
51+
}
52+
53+
/* WAL must exist and be non-empty before the checkpoint call. */
54+
struct stat st_before;
55+
int rc_stat = stat(wal_path, &st_before);
56+
ASSERT_EQ(rc_stat, 0);
57+
ASSERT(st_before.st_size > 0);
58+
59+
/* Under SQLITE_CHECKPOINT_TRUNCATE the WAL would be ftruncate()d to 0
60+
* bytes on success. Under SQLITE_CHECKPOINT_PASSIVE the file size is
61+
* preserved (frames marked, not removed). */
62+
int rc_ckpt = cbm_store_checkpoint(s);
63+
ASSERT_EQ(rc_ckpt, 0); /* CBM_STORE_OK */
64+
65+
struct stat st_after;
66+
rc_stat = stat(wal_path, &st_after);
67+
ASSERT_EQ(rc_stat, 0);
68+
ASSERT(st_after.st_size > 0);
69+
70+
cbm_store_close(s);
71+
unlink(db_path);
72+
unlink(wal_path);
73+
unlink(shm_path);
74+
PASS();
75+
}
76+
77+
SUITE(store_checkpoint) {
78+
RUN_TEST(checkpoint_does_not_truncate_wal);
79+
}

0 commit comments

Comments
 (0)