# How code-review-graph Handles Concurrent Read Access to Its SQLite Graph Store

> Learn how code-review-graph ensures safe concurrent read access to its SQLite graph store using WAL mode, busy timeouts, and thread-safe connections for simultaneous processing.

- Repository: [Tirth Kanani/code-review-graph](https://github.com/tirth8205/code-review-graph)
- Tags: internals
- Published: 2026-08-11

---

**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:

```python
self._conn.execute("PRAGMA journal_mode=WAL")

```

As implemented in [`code_review_graph/graph.py`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/graph.py) ([lines 88-98](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/graph.py#L88-L98)), 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:

```python
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:

```python
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`](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/graph.py) ([lines 50-58](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/graph.py#L50-L58)) implements this:

```python
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](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/graph.py#L124-L150)) 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](https://github.com/tirth8205/code-review-graph/blob/main/code_review_graph/graph.py#L21-L26)):

```python
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

```python
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`](https://github.com/tirth8205/code-review-graph/blob/main/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`](https://github.com/tirth8205/code-review-graph/blob/main/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`](https://github.com/tirth8205/code-review-graph/blob/main/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.