Skip to content

fix: give each thread its own SQLite connection - #54

Merged
JarbasAl merged 1 commit into
devfrom
fix/thread-safe-sqlite-connections
Aug 3, 2026
Merged

JarbasAl merged 1 commit into
devfrom
fix/thread-safe-sqlite-connections

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

The plugin shared one sqlite3.Connection between all callers. A connection owns the transaction state, so when two threads use it at the same time one threads COMMIT ends the other threads transaction and resets its in-flight statements. Reads were not guarded at all, and search_by_value wrapped its SELECT in with self.conn:, which commits on exit — a read committing somebody elses write.

How it was found

Under the Tornado network backend every admission and every inbound frame ran on one IOLoop thread, so this never showed. A threaded backend (webrockets, JarbasHiveMind/hivemind-websocket-protocol#43) dispatches admission and receive from several Rust worker threads at once. Measured on a 50-client load test against the previous code:

symptom count
search_by_value: bad parameter or other API misuse 98
add_item: NOT NULL constraint failed: clients.api_key 11
downstream Client provided an invalid api key (valid keys) 3

Same rig, same N, Tornado backend: 0 of each. Client names in the rejection logs came out mangled (lt93::94::::), which is a corrupted row read, not a client-side problem.

The fix

Each thread opens its own connection on first use (threading.local). WAL already allows many readers beside one writer, and _write_lock still serialises writers in-process so the connections do not fight over the file lock; busy_timeout covers any that do. search_by_value no longer opens a transaction around its SELECT.

conn becomes a property. The setter adopts a connection for the calling thread only, which keeps the existing test helpers working.

Tests

TestSQLiteDBThreadSafety.test_concurrent_read_write_is_clean runs 16 readers and 8 writers against a file-backed DB. It fails on the previous code (IndexError: tuple index out of range and friends) and passes on this one. Full suite: 63 passed, 3 skipped.

Verification in the load rig

With this branch on PYTHONPATH, the webrockets rig at N=50 logs 0 SQLite errors, down from 98/100 across two pre-fix repetitions.

Summary by CodeRabbit

  • Bug Fixes
    • Improved database reliability when multiple operations run concurrently.
    • Added configurable busy-timeout handling to reduce transient database lock errors.
    • Ensured database connections are isolated per thread for safer parallel access.
    • Prevented empty encryption passwords from being accepted.
    • Improved connection lifecycle and cleanup behavior.
    • Enabled WAL mode to support more reliable simultaneous reads and writes.

The plugin shared one sqlite3.Connection between all callers. A connection
owns the transaction state, so when two threads use it at the same time one
thread's COMMIT ends the other thread's transaction and resets its in-flight
statements. Reads were not guarded at all, and search_by_value wrapped its
SELECT in `with self.conn:`, which commits on exit - a read committing
somebody else's write.

Under the Tornado network backend everything ran on one IOLoop thread, so
this never showed. Under a threaded backend it shows immediately: a 50-client
load test logged 98 x "bad parameter or other API misuse" from
search_by_value and 11 x "NOT NULL constraint failed: clients.api_key" from
add_item, and valid clients were refused because their api_key lookup
returned nothing.

Each thread now opens its own connection on first use. WAL already allows
many readers beside one writer, and _write_lock still serialises writers
in-process, so the connections do not fight over the file lock; busy_timeout
covers any that do. The SELECT no longer opens a transaction.

The new concurrency test fails on the previous code and passes on this one.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 52de5522-1c0e-4c13-bdc8-b4240ad48a4c

📥 Commits

Reviewing files that changed from the base of the PR and between 78dff6d and 13b1b48.

📒 Files selected for processing (2)
  • hivemind_sqlite_database/__init__.py
  • tests/test_sqlitedb.py

📝 Walkthrough

Walkthrough

SQLiteDB now uses lazy, thread-local SQLite or SQLCipher connections with WAL mode and a configurable busy timeout. It validates encryption passwords, supports per-thread connection injection and closing, avoids implicit read commits, and adds concurrent read/write coverage.

Changes

SQLite concurrency support

Layer / File(s) Summary
Connection configuration and initialization
hivemind_sqlite_database/__init__.py
SQLiteDB exposes BUSY_TIMEOUT_MS, stores the database path, validates encryption passwords, initializes the write lock, and defers connection creation.
Thread-local connection lifecycle
hivemind_sqlite_database/__init__.py
Connections are created and configured per thread. The implementation supports connection injection and closes only the current thread’s connection.
Read execution and concurrency validation
hivemind_sqlite_database/__init__.py, tests/test_sqlitedb.py
search_by_value no longer commits through the connection context manager. The new test exercises concurrent readers and writers and checks result and row-count consistency.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReaderThreads
  participant WriterThreads
  participant SQLiteDB
  participant SQLiteDatabase
  ReaderThreads->>SQLiteDB: Request thread-local connection
  SQLiteDB->>SQLiteDatabase: Open connection with WAL and busy timeout
  SQLiteDB-->>ReaderThreads: Return connection
  WriterThreads->>SQLiteDB: Upsert API-key data
  SQLiteDB->>SQLiteDatabase: Write data
  ReaderThreads->>SQLiteDB: Search by value
  SQLiteDB->>SQLiteDatabase: Execute read query
  SQLiteDatabase-->>ReaderThreads: Return results
Loading

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/thread-safe-sqlite-connections

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the fix label Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Look what I found! The automated check results are in. 🔍

I've aggregated the results of the automated checks for this PR below.

📊 Coverage

A bird's eye view of your test coverage landscape. 🦅

✅ 84.8% total coverage

Per-file coverage (2 files)
File Coverage Missing lines
hivemind_sqlite_database/version.py 0.0% 5
hivemind_sqlite_database/__init__.py 87.1% 24

Full report: download the coverage-report artifact.

🔒 Security (pip-audit)

Ensuring our security logs are being captured. 🪵

✅ No known vulnerabilities found (70 packages scanned).

⚖️ License Check

Ensuring our CLA requirements are met. 🖋️

✅ No license violations found.

Policy: Apache 2.0 (universal donor). StrongCopyleft / NetworkCopyleft / WeakCopyleft / Other / Error categories fail. MPL allowed.

📋 Repo Health

Scanning for any signs of 'copy-paste' obesity. 🍕

✅ All required files present.

Latest Version: 0.4.1a1

✅ hivemind_sqlite_database/version.py — Version file
✅ README.md — README
✅ LICENSE.md — License file (consider renaming to LICENSE)
✅ pyproject.toml — pyproject.toml
⚠️ setup.py — setup.py
✅ CHANGELOG.md — Changelog
✅ hivemind_sqlite_database/version.py has valid version block markers

🔨 Build Tests

Ensuring no loose screws in the assembly. 🔩

✅ All versions pass

Python Build Install Tests
3.10 ✅ ✅ ✅
3.11 ✅ ✅ ✅
3.12 ✅ ✅ ✅
3.13 ✅ ✅ ✅
3.14 ✅ ✅ ✅

Delivered by the OVOS Automated Messenger 🕊️

@JarbasAl
JarbasAl marked this pull request as ready for review August 3, 2026 22:50
@JarbasAl
JarbasAl merged commit 9fbd51b into dev Aug 3, 2026
15 checks passed
@JarbasAl
JarbasAl deleted the fix/thread-safe-sqlite-connections branch August 3, 2026 22:50
@github-actions github-actions Bot added fix and removed fix labels Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant