Skip to content

Fix: enable WAL and batch writes in TelemetryStore to avoid SQLITE_BUSY under concurrency (fixes #560)#597

Open
SoulSniper-V2 wants to merge 3 commits into
open-jarvis:mainfrom
SoulSniper-V2:fix-telemetry-wal-560
Open

Fix: enable WAL and batch writes in TelemetryStore to avoid SQLITE_BUSY under concurrency (fixes #560)#597
SoulSniper-V2 wants to merge 3 commits into
open-jarvis:mainfrom
SoulSniper-V2:fix-telemetry-wal-560

Conversation

@SoulSniper-V2

Copy link
Copy Markdown
Contributor

What does this PR do?

Resolves Issue #560 and fully closes out parent Issue #219 alongside PR #570.

The TelemetryStore was previously missing PRAGMA journal_mode=WAL which made it susceptible to SQLITE_BUSY errors under concurrent inference load. Furthermore, every telemetry insert was immediately calling commit(), forcing high fsync I/O overhead.

Changes

  • WAL Mode Enabled: Set PRAGMA journal_mode=WAL and PRAGMA synchronous=NORMAL when initializing TelemetryStore so it correctly mimics TraceStore.
  • In-Memory Batching: Added a threading.Lock() to TelemetryStore to buffer incoming records in self._telemetry_batch and self._mining_batch.
  • Reduced Disk I/O: Modifed record() and record_mining_stats() to hold onto records until the batch_size (default: 50) is reached.
  • Flushing Behavior: Added a thread-safe flush() method that uses SQLite's fast executemany() operation. This method is called automatically when hitting the batch limit, when querying (list_recent), and when closing the DB connection.
  • Unit Testing: Added test_batching_delays_commit to test_store.py to ensure batching logic correctly defers disk commits.

Testing

  • Added unit/integration tests
  • Ran locally
  • Verified endpoints

Fixes Issue

Fixes #560

Copilot AI review requested due to automatic review settings June 26, 2026 11:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates TelemetryStore’s SQLite usage to better tolerate concurrent write load by enabling WAL mode and batching inserts, reducing commit/fsync frequency and aiming to prevent SQLITE_BUSY errors under inference concurrency.

Changes:

  • Enable SQLite WAL mode and set synchronous=NORMAL on TelemetryStore initialization.
  • Buffer telemetry + mining stats rows in-memory and flush with executemany() when the batch threshold is reached (and on query/close).
  • Add a unit test to verify that commits are deferred until the batch size is hit.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/openjarvis/telemetry/store.py Enables WAL + adds lock-protected batching/flush logic for telemetry/mining inserts.
tests/telemetry/test_store.py Adds a regression test asserting batched writes delay persistence until flush/threshold.
Comments suppressed due to low confidence (1)

src/openjarvis/telemetry/store.py:315

  • _select_dicts() executes SQL without holding the store lock. To keep the single sqlite3.Connection safe under multi-threaded access (even with check_same_thread=False), reads should also be serialized with the same lock used for writes/flush.
    def _select_dicts(self, sql: str, params: tuple[Any, ...]) -> list[dict[str, Any]]:
        cur = self._conn.execute(sql, params)
        columns = [desc[0] for desc in cur.description]
        return [dict(zip(columns, row)) for row in cur.fetchall()]

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/openjarvis/telemetry/store.py Outdated
Comment on lines +149 to +153
def __init__(self, db_path: str | Path, batch_size: int = 50) -> None:
self._db_path = str(db_path)
self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
self._conn.execute("PRAGMA journal_mode=WAL")
self._conn.execute("PRAGMA synchronous=NORMAL")
Comment thread src/openjarvis/telemetry/store.py Outdated
Comment on lines +159 to +162
self._lock = threading.Lock()
self._batch_size = batch_size
self._telemetry_batch: list[tuple[Any, ...]] = []
self._mining_batch: list[tuple[Any, ...]] = []
Comment thread src/openjarvis/telemetry/store.py Outdated
Comment on lines 301 to 304
def close(self) -> None:
"""Close the underlying SQLite connection."""
self.flush()
self._conn.close()
Comment thread src/openjarvis/telemetry/store.py Outdated
Comment on lines 308 to 310
def _fetchall(self, sql: str = "SELECT * FROM telemetry") -> list:
self.flush()
return self._conn.execute(sql).fetchall()
Comment thread tests/telemetry/test_store.py Outdated
Comment on lines +160 to +170
import sqlite3

conn = sqlite3.connect(db_path)
assert len(conn.execute("SELECT * FROM telemetry").fetchall()) == 0
conn.close()

# Hit batch size
store.record(rec)
conn = sqlite3.connect(db_path)
assert len(conn.execute("SELECT * FROM telemetry").fetchall()) == 2
conn.close()
Comment on lines +152 to +155
def test_batching_delays_commit(self, tmp_path: Path) -> None:
db_path = tmp_path / "test.db"
store = TelemetryStore(db_path, batch_size=2)
rec = TelemetryRecord(timestamp=time.time(), model_id="m1", engine="e1")
@ElliotSlusky

ElliotSlusky commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Thank you @SoulSniper-V2! Can you fix the tests and merge conflicts?

@SoulSniper-V2

Copy link
Copy Markdown
Contributor Author

Hey @ElliotSlusky! Fixed — merge conflicts are resolved and all 13 tests pass (including the new WAL and concurrency tests from main). I also integrated main's busy_timeout=5000 alongside the batching logic. Ready for another look!

…ches

Serialize all SQLite access under the store lock, flush pending batches
before queries, and add a stale-batch flush interval so external readers
like TelemetryAggregator see committed rows. Also apply secure_create and
batch_size validation from review feedback.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Fix: enable WAL and batch writes in TelemetryStore to avoid SQLITE_BUSY under concurrency

4 participants