How the Store Module Handles Concurrent Access to Graph Data in Codebase-Memory-MCP
The store module in DeusData/codebase-memory-mcp delegates concurrency control to SQLite's WAL mode and busy timeout mechanisms, while explicitly requiring that a single cbm_store_t handle never be accessed from multiple threads simultaneously—either isolate handles per thread or provide external synchronization.
The codebase-memory-mcp project implements a persistent graph store for code knowledge using SQLite as the underlying engine. When multiple threads or processes need to read or write graph data concurrently, the module relies on a specific contract: SQLite handles the database-level locking, while the application layer ensures handle-level isolation. This design avoids hidden race conditions but places clear constraints on how developers use the API.
Thread Safety Contract and Design Constraints
The concurrency model is declared explicitly in the public header. According to src/store/store.h, a single store handle is strictly not thread-safe:
/* store.h — Opaque SQLite graph store for code knowledge graphs.
*
* Thread safety: a single store handle must not be used concurrently.
* Use one store per thread or external synchronization.
*/
This contract creates two valid usage patterns:
- One store per thread – Each thread opens its own
cbm_store_thandle viacbm_store_open()orcbm_store_open_path(). - External synchronization – Multiple threads share one handle, but wrap all access in a mutex or other locking primitive.
Violating this contract by sharing a handle across threads without synchronization results in undefined behavior, as the module does not implement internal locking for the handle structure itself.
SQLite Concurrency Primitives
When initializing a store for normal read-write access, the module configures SQLite to use WAL (Write-Ahead Logging) mode. This allows readers to proceed without blocking on writers, and vice versa, while maintaining ACID guarantees.
In src/store/store.c, the configure_pragmas() function sets this up:
rc = exec_sql(s, "PRAGMA journal_mode = WAL;");
To prevent immediate failures when the database is locked, the module also sets a 10-second busy timeout. This causes write operations to wait rather than failing immediately with SQLITE_BUSY:
rc = exec_sql(s, "PRAGMA busy_timeout = 10000;");
These pragmas ensure that while the module itself is not thread-safe, the underlying SQLite database can safely coordinate access across multiple processes or threads that each hold their own connection.
Transaction Boundaries for Atomicity
For operations that must be atomic, the module provides explicit transaction helpers that wrap SQLite's locking protocol. These functions block according to the busy timeout if another writer holds the lock:
cbm_store_begin()– Starts anIMMEDIATEtransaction, acquiring a write lock eagerly.cbm_store_commit()– Finalizes the transaction and releases locks.cbm_store_rollback()– Aborts the transaction and releases locks.
The implementation in src/store/store.c is straightforward:
int cbm_store_begin(cbm_store_t *s) { return exec_sql(s, "BEGIN IMMEDIATE;"); }
int cbm_store_commit(cbm_store_t *s) { return exec_sql(s, "COMMIT;"); }
int cbm_store_rollback(cbm_store_t *s) { return exec_sql(s, "ROLLBACK;"); }
Using BEGIN IMMEDIATE rather than DEFERRED ensures that the write lock is acquired at the start, preventing potential deadlocks in concurrent scenarios where multiple connections might upgrade from read to write locks simultaneously.
Optimizing Bulk Write Operations
When performing large batches of inserts or updates, the module offers bulk transaction helpers that temporarily relax durability settings for performance while maintaining WAL safety.
The cbm_store_begin_bulk() function in src/store/store.c disables synchronous writes and expands the cache:
int cbm_store_begin_bulk(cbm_store_t *s) {
int rc = exec_sql(s, "PRAGMA synchronous = OFF;");
if (rc != CBM_STORE_OK) return rc;
return exec_sql(s, "PRAGMA cache_size = -65536;");
}
This optimization speeds up bulk operations significantly. Because the module remains in WAL mode, concurrent readers can still access the database during the bulk write, though they may see the state as of the last committed transaction. The caller must pair this with cbm_store_end_bulk() to restore normal settings and commit the transaction.
Read-Only Access Patterns
For scenarios requiring only query access, the module provides cbm_store_open_path_query(), which opens the database in read-only mode. This path eliminates write locks entirely and allows unlimited concurrent readers without any coordination overhead.
The implementation attempts to open with SQLITE_OPEN_READONLY. If the filesystem cannot create the WAL shared memory file (-shm), it falls back to an immutable URI:
rc = sqlite3_open_v2(uri, &s->db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL);
When the immutable flag is used, SQLite bypasses WAL entirely and assumes the database file never changes, providing zero-overhead concurrent access for read-only workloads.
SQL Injection and Security Safeguards
To prevent malicious SQL from attaching additional databases or modifying the filesystem through SQL injection, the module installs a SQLite authorizer callback in src/store/store.c:
static int store_authorizer(void *user_data, int action, const char *p3, const char *p4,
const char *p5, const char *p6) {
switch (action) {
case SQLITE_ATTACH:
case SQLITE_DETACH:
return SQLITE_DENY;
default:
return SQLITE_OK;
}
}
This denies ATTACH and DETACH statements, ensuring that even if an attacker could inject SQL through the graph API, they cannot create new database files or access other databases on the system.
Summary
- Handle isolation is mandatory – The
cbm_store_tstruct is not thread-safe; use one handle per thread or external mutexes. - WAL mode enables concurrency – SQLite's Write-Ahead Logging allows readers and writers to coexist, configured via
PRAGMA journal_mode = WAL. - Busy timeout prevents spurious failures – A 10-second timeout lets writers wait for locks rather than failing immediately.
- Explicit transactions control locking –
cbm_store_begin()usesBEGIN IMMEDIATEto acquire locks upfront. - Bulk writes optimize performance – Temporary
synchronous = OFFand cache expansion during bulk operations maximize throughput. - Read-only access is conflict-free –
cbm_store_open_path_query()supports unlimited concurrent readers with immutable fallback. - Security is enforced at the SQL layer – An authorizer callback blocks
ATTACHandDETACHoperations.
Frequently Asked Questions
Is the store module thread-safe?
No. While the underlying SQLite database supports multi-threading and multi-process access, the cbm_store_t handle itself is not thread-safe. You must either open a separate store handle for each thread or protect all access to a shared handle with a mutex. This contract is explicitly documented in src/store/store.h.
What happens if two threads try to write to the same store handle simultaneously?
Simultaneous access from two threads to one cbm_store_t handle results in undefined behavior, including potential data corruption or crashes. The module does not implement internal locking. For concurrent write access, use one handle per thread (each opening the same database file) and rely on SQLite's WAL mode and busy timeout to coordinate at the database level.
How does the module handle bulk imports without blocking readers?
During bulk operations initiated with cbm_store_begin_bulk(), the module temporarily sets PRAGMA synchronous = OFF and increases the cache size. Because it maintains WAL mode, readers can continue accessing the database while the bulk write proceeds. Readers see a consistent snapshot of the data as of the last committed transaction, and the bulk write commits atomically when complete.
Can multiple processes read the graph database simultaneously?
Yes. For read-only access, use cbm_store_open_path_query(), which opens the database with SQLITE_OPEN_READONLY. If the filesystem supports it, this uses WAL mode for consistency. If not, the module falls back to an immutable URI, allowing unlimited concurrent readers across processes with no locking overhead.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →