Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions hermes_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2083,10 +2083,13 @@ def _get_read_conn(self) -> Optional[sqlite3.Connection]:
if getattr(self._read_local, "failed", False):
return None
try:
# close() drains worker-created connections from its caller, so
# readers must permit cross-thread close after the worker exits.
conn = _connect_tracked_db(
f"file:{self.db_path}?mode=ro",
tracking_path=self.db_path,
uri=True,
check_same_thread=False,
timeout=5.0,
isolation_level=None,
)
Expand Down Expand Up @@ -2651,6 +2654,11 @@ def close(self):
for conn in read_conns:
try:
conn.close()
except sqlite3.ProgrammingError as exc:
logger.warning(
"SessionDB.close() could not close a read connection: %s",
exc,
)
except Exception:
pass
self._read_local.conn = None
Expand Down
22 changes: 22 additions & 0 deletions tests/test_session_db_read_path_split.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
back to the legacy locked path when WAL or the read connection is missing.
"""

from concurrent.futures import ThreadPoolExecutor
from contextlib import suppress
import sqlite3
import threading

import pytest
Expand Down Expand Up @@ -129,3 +132,22 @@ def reader():
assert done["view"]["window"]
finally:
db._lock.release()


@pytest.mark.requires_wal
def test_close_drains_read_conn_created_by_finished_worker(tmp_path):
"""close() must reclaim a read connection created by another thread."""
d = SessionDB(db_path=tmp_path / "state.db")
try:
with ThreadPoolExecutor(max_workers=1) as pool:
conn = pool.submit(d._get_read_conn).result(timeout=5.0)
assert conn is not None

d.close()

assert not d._read_conns
with pytest.raises(sqlite3.ProgrammingError, match="closed database"):
conn.execute("SELECT 1")
finally:
with suppress(Exception):
d.close()