How code-review-graph Handles Concurrent Read Access to Its SQLite Graph Store
code-review-graph enables safe concurrent reads by configuring SQLite with Write-Ahead Logging (WAL) mode, a 5-second busy timeout, and thread-safe connections, allowing multiple readers to process the knowledge graph simultaneously without blocking each other.
The code-review-graph repository stores its entire source-code knowledge graph in a single SQLite file. According to the tirth8205/code-review-graph source code, concurrent read access is managed entirely through SQLite-native mechanisms combined with defensive connection settings in the GraphStore class. This design eliminates lock contention during read-heavy operations while maintaining atomic write consistency.
WAL Mode: The Foundation of Concurrent Reads
The GraphStore class enables Write-Ahead Logging (WAL) immediately upon opening a database connection. This SQLite feature fundamentally changes how concurrency works:
self._conn.execute("PRAGMA journal_mode=WAL")
As implemented in code_review_graph/graph.py (lines 88-98), WAL mode provides three critical benefits for concurrent access:
- Readers never block each other — Any number of threads can execute
SELECTstatements simultaneously - Readers don't block writers — A writer can prepare a transaction while readers continue
- Writers only block briefly — The exclusive lock is held only during commit, not during the entire write operation
Connection Configuration for Thread Safety
The SQLite connection is explicitly configured to support multi-threaded access:
self._conn = sqlite3.connect(
str(self.db_path), timeout=30, check_same_thread=False,
isolation_level=None, # Disable implicit transactions (#135)
)
The key settings here are:
check_same_thread=False— Removes Python's default restriction that would prevent sharing the connection across threadsisolation_level=None— Disables implicit transaction management, putting the application in direct controltimeout=30— Sets a 30-second connection-level timeout for acquiring locks
Graceful Lock Handling with Busy Timeout
When a reader encounters a lock held by a writer, SQLite automatically retries rather than failing immediately:
self._conn.execute("PRAGMA busy_timeout=5000")
This 5-second busy timeout prevents "database is locked" errors under normal load. Readers wait briefly for the writer to complete, then proceed with the now-committed data.
Write Isolation Through IMMEDIATE Transactions
All batch write operations in code-review-graph use explicit BEGIN IMMEDIATE transactions. The _begin_immediate method in code_review_graph/graph.py (lines 50-58) implements this:
def _begin_immediate(self) -> None:
if self._conn.in_transaction:
logger.warning("Rolling back uncommitted transaction before BEGIN IMMEDIATE")
self._conn.rollback()
self._conn.execute("BEGIN IMMEDIATE")
This approach guarantees:
- Early write lock acquisition — No other writer can start mid-batch
- Uninterrupted read access — Existing readers continue on their pre-write snapshot
- Atomic visibility — New data appears only after full commit
Write methods like store_file_nodes_edges, store_file_batch, and remove_files_permanently all invoke this pattern.
Lock-Free Read Path
Read operations in code-review-graph use simple SELECT statements without explicit transactions. Methods such as get_node, iter_nodes_by_file, and search_nodes (lines 124-150) operate directly on the WAL snapshot, requiring no locks and causing no contention.
Cache Synchronization
The optional NetworkX cache (self._nxg_cache) is the only structure requiring explicit locking. A threading.Lock (self._cache_lock) protects cache invalidation after writes (lines 21-26):
def _invalidate_cache(self) -> None:
with self._cache_lock:
self._nxg_cache = None
The lock is held only during invalidation, keeping the read path completely lock-free.
Practical Example: Concurrent Reads with Background Writes
from code_review_graph.graph import GraphStore
import threading
# ----------------------------------------------------------------------
# 1️⃣ Open a shared GraphStore (single SQLite file)
# ----------------------------------------------------------------------
store = GraphStore("/tmp/crg.db")
# ----------------------------------------------------------------------
# 2️⃣ Simple concurrent read (multiple threads only SELECT)
# ----------------------------------------------------------------------
def read_node(qname: str):
node = store.get_node(qname)
print(f"Node {qname!r}: {node}")
threads = [
threading.Thread(target=read_node, args=("src/main.py::MyClass",)),
threading.Thread(target=read_node, args=("src/utils.py::helper",)),
threading.Thread(target=read_node, args=("src/main.py::MyClass::method",)),
]
for t in threads:
t.start()
for t in threads:
t.join()
# ----------------------------------------------------------------------
# 3️⃣ Write that co‑exists with readers (IMMEDIATE transaction)
# ----------------------------------------------------------------------
def replace_file_data():
nodes = [...] # list[NodeInfo] from a parser run
edges = [...] # list[EdgeInfo]
store.store_file_nodes_edges("src/main.py", nodes, edges, fhash="deadbeef")
write_thread = threading.Thread(target=replace_file_data)
write_thread.start()
# Readers can continue running while the writer holds the lock.
# They will see the *old* version until the writer commits.
for t in threads:
t.join()
write_thread.join()
store.close()
Concurrency Behavior Summary
| Scenario | SQLite Mechanism | Effect in code-review-graph |
|---|---|---|
| Multiple threads read simultaneously | WAL snapshot isolation | Purely concurrent, no blocking |
One thread writes with BEGIN IMMEDIATE |
Exclusive write lock | Readers see pre-write snapshot; no interruption |
| Reader starts during active write | 5-second busy timeout | Transparent retry, then sees committed data |
| Write commits | WAL log flush | Subsequent readers immediately see new data |
Summary
- WAL mode in
code_review_graph/graph.pyenables multiple concurrent readers without lock contention check_same_thread=Falsewith proper isolation settings allows safe cross-thread connection sharing- 5-second busy timeout prevents reader failures during brief write conflicts
BEGIN IMMEDIATEtransactions isolate writes while preserving read availability- Lock-free read methods (
get_node,iter_nodes_by_file,search_nodes) maximize throughput - Minimal cache locking (
_cache_lock) protects only the optional NetworkX cache during invalidation
Frequently Asked Questions
Does code-review-graph use a connection pool for concurrent access?
No. The GraphStore class uses a single SQLite connection shared across threads. According to the source code in code_review_graph/graph.py, check_same_thread=False makes this safe because SQLite's own locking mechanisms (WAL mode and busy timeout) handle concurrency at the database level, not the Python connection level.
Can readers see partially written data during a batch update?
No. Because all batch writes use BEGIN IMMEDIATE, they operate within a single SQLite transaction. Readers see either the complete pre-write state or the complete post-write state—never an intermediate mix. This is enforced by WAL snapshot isolation.
What happens if multiple threads try to write simultaneously?
The first writer to execute BEGIN IMMEDIATE acquires the exclusive write lock immediately. Subsequent writers will block on their own BEGIN IMMEDIATE calls until the first writer commits or rolls back, then proceed in FIFO order. The busy timeout applies here as well.
Is the in-memory NetworkX cache thread-safe?
Yes, but with minimal locking. The _cache_lock mutex in code_review_graph/graph.py protects only cache invalidation after writes. Read access to the cache is unsynchronized for performance, assuming the reference is replaced atomically during invalidation.
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 →