Skip to content

Commit 093707c

Browse files
edwardmhughesDeusData
authored andcommitted
feat(store): expose mmap_size via CBM_SQLITE_MMAP_SIZE env
Hard-coded `PRAGMA mmap_size = 67108864` in configure_pragmas() left no path for users running multiple cbm-mcp instances against the same cache to opt out of memory-mapped I/O. On macOS, when one instance's checkpoint or reindex truncates the DB file under another instance's live mmap, accessing the now-missing pages raises SIGBUS, taking the process down. Setting CBM_SQLITE_MMAP_SIZE=0 reverts to read()/pread() I/O, which returns recoverable SQLITE_IOERR instead of crashing the process. - Default unchanged (67108864 / 64 MB). No behavior change for single-instance users. - Malformed values (non-numeric, partial-numeric) fall back to the default rather than failing the store open. - Negative values clamp to 0. - New tests: tests/test_store_pragmas.c covers all five resolver paths plus an integration smoke that opens a file-backed store with mmap disabled. Empirical evidence: 9 SIGBUS crash reports collected on macOS arm64 v0.6.0 in a 14-hour window, all signature 'cluster_pagein past EOF' with stacks bottoming in SQLite btree code under the watcher thread's incremental-index pipeline.
1 parent a338ff3 commit 093707c

5 files changed

Lines changed: 132 additions & 2 deletions

File tree

Makefile.cbm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,8 @@ TEST_STORE_SRCS = \
281281
tests/test_store_edges.c \
282282
tests/test_store_search.c \
283283
tests/test_store_arch.c \
284-
tests/test_store_bulk.c
284+
tests/test_store_bulk.c \
285+
tests/test_store_pragmas.c
285286

286287
TEST_CYPHER_SRCS = \
287288
tests/test_cypher.c

src/store/store.c

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,24 @@ static int create_user_indexes(cbm_store_t *s) {
299299
return exec_sql(s, sql);
300300
}
301301

302+
int64_t cbm_store_resolve_mmap_size(void) {
303+
enum { MMAP_DEFAULT = 67108864, BASE_10 = 10 }; /* default 64 MB; decimal radix */
304+
char buf[ST_BUF_64];
305+
if (cbm_safe_getenv("CBM_SQLITE_MMAP_SIZE", buf, sizeof(buf), NULL) == NULL) {
306+
return (int64_t)MMAP_DEFAULT;
307+
}
308+
char *end = NULL;
309+
long long parsed = strtoll(buf, &end, BASE_10);
310+
if (end == buf || *end != '\0') {
311+
/* Malformed — fall back to default rather than fail the store open. */
312+
return (int64_t)MMAP_DEFAULT;
313+
}
314+
if (parsed < 0) {
315+
return 0;
316+
}
317+
return (int64_t)parsed;
318+
}
319+
302320
static int configure_pragmas(cbm_store_t *s, bool in_memory) {
303321
int rc;
304322
rc = exec_sql(s, "PRAGMA foreign_keys = ON;");
@@ -325,7 +343,10 @@ static int configure_pragmas(cbm_store_t *s, bool in_memory) {
325343
if (rc != CBM_STORE_OK) {
326344
return rc;
327345
}
328-
rc = exec_sql(s, "PRAGMA mmap_size = 67108864;"); /* CBM_SZ_64 MB */
346+
char mmap_sql[ST_BUF_64];
347+
snprintf(mmap_sql, sizeof(mmap_sql), "PRAGMA mmap_size = %lld;",
348+
(long long)cbm_store_resolve_mmap_size());
349+
rc = exec_sql(s, mmap_sql);
329350
}
330351
return rc;
331352
}

src/store/store.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,15 @@ int cbm_store_create_indexes(cbm_store_t *s);
250250
/* Force WAL checkpoint + PRAGMA optimize. */
251251
int cbm_store_checkpoint(cbm_store_t *s);
252252

253+
/* Resolve the mmap_size pragma value applied to on-disk stores from the
254+
* CBM_SQLITE_MMAP_SIZE environment variable. Defaults to 67108864 (64 MB)
255+
* when the variable is unset, malformed, or partially numeric. Negative
256+
* values clamp to 0 (which disables mmap and reverts to read()/pread()
257+
* I/O — recoverable SQLITE_IOERR instead of SIGBUS when concurrent
258+
* processes truncate the DB file under live mappings). Exposed for
259+
* testability. */
260+
int64_t cbm_store_resolve_mmap_size(void);
261+
253262
/* ── Dump / Restore ─────────────────────────────────────────────── */
254263

255264
/* Dump in-memory database to a file. */

tests/test_main.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ extern void suite_py_lsp_scale(void);
5252
extern void suite_ts_lsp(void);
5353
extern void suite_store_arch(void);
5454
extern void suite_store_bulk(void);
55+
extern void suite_store_pragmas(void);
5556
extern void suite_traces(void);
5657
extern void suite_configlink(void);
5758
extern void suite_infrascan(void);
@@ -89,6 +90,7 @@ int main(void) {
8990
RUN_SUITE(store_edges);
9091
RUN_SUITE(store_search);
9192
RUN_SUITE(store_bulk);
93+
RUN_SUITE(store_pragmas);
9294

9395
/* Cypher (M6) */
9496
RUN_SUITE(cypher);

tests/test_store_pragmas.c

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* test_store_pragmas.c — Tests for SQLite pragma resolution.
3+
*
4+
* Validates that the CBM_SQLITE_MMAP_SIZE env var controls the mmap_size
5+
* pragma applied to on-disk stores. Default behavior (env unset) must
6+
* remain 64 MB. Setting the env to 0 disables memory-mapped I/O so
7+
* concurrent processes that truncate the DB file under a sibling's live
8+
* mapping return SQLITE_IOERR instead of crashing the process with SIGBUS.
9+
*/
10+
#include "../src/foundation/compat.h"
11+
#include "test_framework.h"
12+
#include "test_helpers.h"
13+
#include <store/store.h>
14+
#include <stdio.h>
15+
#include <stdlib.h>
16+
#include <string.h>
17+
#include <unistd.h>
18+
19+
static void clear_mmap_env(void) {
20+
unsetenv("CBM_SQLITE_MMAP_SIZE");
21+
}
22+
23+
TEST(mmap_size_default_when_unset) {
24+
clear_mmap_env();
25+
ASSERT_EQ(cbm_store_resolve_mmap_size(), 67108864LL);
26+
PASS();
27+
}
28+
29+
TEST(mmap_size_zero_disables_mmap) {
30+
setenv("CBM_SQLITE_MMAP_SIZE", "0", 1);
31+
ASSERT_EQ(cbm_store_resolve_mmap_size(), 0LL);
32+
clear_mmap_env();
33+
PASS();
34+
}
35+
36+
TEST(mmap_size_explicit_value) {
37+
setenv("CBM_SQLITE_MMAP_SIZE", "1048576", 1);
38+
ASSERT_EQ(cbm_store_resolve_mmap_size(), 1048576LL);
39+
clear_mmap_env();
40+
PASS();
41+
}
42+
43+
TEST(mmap_size_negative_clamped_to_zero) {
44+
setenv("CBM_SQLITE_MMAP_SIZE", "-1", 1);
45+
ASSERT_EQ(cbm_store_resolve_mmap_size(), 0LL);
46+
clear_mmap_env();
47+
PASS();
48+
}
49+
50+
TEST(mmap_size_garbage_falls_back_to_default) {
51+
setenv("CBM_SQLITE_MMAP_SIZE", "not-a-number", 1);
52+
ASSERT_EQ(cbm_store_resolve_mmap_size(), 67108864LL);
53+
clear_mmap_env();
54+
PASS();
55+
}
56+
57+
TEST(mmap_size_partial_garbage_falls_back_to_default) {
58+
setenv("CBM_SQLITE_MMAP_SIZE", "123abc", 1);
59+
ASSERT_EQ(cbm_store_resolve_mmap_size(), 67108864LL);
60+
clear_mmap_env();
61+
PASS();
62+
}
63+
64+
/* Integration smoke: opening a file-backed store with mmap_size=0 must
65+
* succeed. Proves the resolver is wired through configure_pragmas(). */
66+
TEST(store_open_with_mmap_disabled) {
67+
setenv("CBM_SQLITE_MMAP_SIZE", "0", 1);
68+
char tmp_path[256];
69+
snprintf(tmp_path, sizeof(tmp_path), "/tmp/cbm_test_pragmas_%d.db", (int)getpid());
70+
unlink(tmp_path);
71+
72+
cbm_store_t *s = cbm_store_open_path(tmp_path);
73+
ASSERT(s != NULL);
74+
cbm_store_close(s);
75+
76+
unlink(tmp_path);
77+
/* WAL/SHM siblings created by the open */
78+
char tmp_wal[300];
79+
char tmp_shm[300];
80+
snprintf(tmp_wal, sizeof(tmp_wal), "%s-wal", tmp_path);
81+
snprintf(tmp_shm, sizeof(tmp_shm), "%s-shm", tmp_path);
82+
unlink(tmp_wal);
83+
unlink(tmp_shm);
84+
85+
clear_mmap_env();
86+
PASS();
87+
}
88+
89+
SUITE(store_pragmas) {
90+
RUN_TEST(mmap_size_default_when_unset);
91+
RUN_TEST(mmap_size_zero_disables_mmap);
92+
RUN_TEST(mmap_size_explicit_value);
93+
RUN_TEST(mmap_size_negative_clamped_to_zero);
94+
RUN_TEST(mmap_size_garbage_falls_back_to_default);
95+
RUN_TEST(mmap_size_partial_garbage_falls_back_to_default);
96+
RUN_TEST(store_open_with_mmap_disabled);
97+
}

0 commit comments

Comments
 (0)