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:

  1. Readers never block each other — Any number of threads can execute SELECT statements simultaneously
  2. Readers don't block writers — A writer can prepare a transaction while readers continue
  3. 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 threads
  • isolation_level=None — Disables implicit transaction management, putting the application in direct control
  • timeout=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.py enables multiple concurrent readers without lock contention
  • check_same_thread=False with proper isolation settings allows safe cross-thread connection sharing
  • 5-second busy timeout prevents reader failures during brief write conflicts
  • BEGIN IMMEDIATE transactions 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →