aa8ceed4b6
Follow-up to the #88347 salvage: after release_all_under() force-closes a profile's shared connection, a store re-created on the same path registers a fresh entry under the same key. A stale holder that later calls close() would pop that fresh entry (its refs were transferred nowhere), letting a third store open a second connection to the same database — exactly the multi-writer contention the shared registry exists to prevent. close() now evicts the registry entry only when it is still its own.
131 lines
5.1 KiB
Python
131 lines
5.1 KiB
Python
"""Regression test for #44037 — holographic provider leaked its SQLite
|
|
connection to GC on shutdown instead of closing it.
|
|
|
|
The corruption-mechanism framing in #44037 (TLS bytes written into the DB via
|
|
an fd-recycle race) was not reproducible from the code: dropping a sqlite
|
|
connection flushes valid pages through SQLite's own VFS, never TLS framing, and
|
|
the provider is at most a *releaser* of DB fds, not the TLS-flushing owner.
|
|
|
|
But the underlying resource-hygiene bug is real and is what this test pins:
|
|
``HolographicMemoryProvider.shutdown()`` must call ``MemoryStore.close()`` so
|
|
the ``check_same_thread=False`` connection's fd is released deterministically
|
|
on shutdown, rather than at a non-deterministic GC time on an arbitrary thread.
|
|
"""
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from plugins.memory.holographic import HolographicMemoryProvider
|
|
|
|
|
|
def _make_provider(tmp_path):
|
|
db_path = str(tmp_path / "memory_store.db")
|
|
provider = HolographicMemoryProvider(config={"db_path": db_path, "hrr_dim": 64})
|
|
provider.initialize(session_id="test-session")
|
|
return provider
|
|
|
|
|
|
def test_shutdown_closes_store_connection(tmp_path):
|
|
provider = _make_provider(tmp_path)
|
|
store = provider._store
|
|
assert store is not None
|
|
conn = store._conn
|
|
|
|
# Connection is live before shutdown.
|
|
conn.execute("SELECT 1").fetchone()
|
|
|
|
provider.shutdown()
|
|
|
|
# References are dropped...
|
|
assert provider._store is None
|
|
assert provider._retriever is None
|
|
|
|
# ...AND the underlying connection was actually closed (not left to GC).
|
|
with pytest.raises(sqlite3.ProgrammingError):
|
|
conn.execute("SELECT 1")
|
|
|
|
|
|
def test_release_all_under_closes_connections_inside_directory_only(tmp_path):
|
|
"""Regression test for #88347 — profile delete must break refcounted handles.
|
|
|
|
``close()`` alone can never free a database held by a live agent (refs >= 1),
|
|
and on Windows an open SQLite handle makes rmtree of the profile directory
|
|
fail with WinError 32. ``release_all_under`` force-closes exactly the shared
|
|
connections under the doomed directory and leaves everything else alone.
|
|
"""
|
|
from plugins.memory.holographic.store import MemoryStore
|
|
|
|
profile_dir = tmp_path / "profiles" / "default-2"
|
|
profile_dir.mkdir(parents=True)
|
|
inside = MemoryStore(db_path=profile_dir / "memory_store.db", hrr_dim=64)
|
|
outside = MemoryStore(db_path=tmp_path / "other" / "memory_store.db", hrr_dim=64)
|
|
inside_conn = inside._conn
|
|
outside_conn = outside._conn
|
|
# Both live before the release; the inside one is held "forever" (refs=1,
|
|
# like a live agent's provider — nobody calls close()).
|
|
inside_conn.execute("SELECT 1").fetchone()
|
|
outside_conn.execute("SELECT 1").fetchone()
|
|
|
|
try:
|
|
released = MemoryStore.release_all_under(profile_dir)
|
|
assert released == 1
|
|
|
|
# The doomed connection is really closed despite refs > 0 ...
|
|
with pytest.raises(sqlite3.ProgrammingError):
|
|
inside_conn.execute("SELECT 1")
|
|
# ... and the registry entry is gone, so a second release finds
|
|
# nothing (the CLI-process no-op case).
|
|
assert str((profile_dir / "memory_store.db").resolve()) not in MemoryStore._shared
|
|
assert MemoryStore.release_all_under(profile_dir) == 0
|
|
# A new store on the same path reopens fresh instead of reusing
|
|
# the dead connection.
|
|
reopened = MemoryStore(db_path=profile_dir / "memory_store.db", hrr_dim=64)
|
|
reopened._conn.execute("SELECT 1").fetchone()
|
|
|
|
# The sibling outside the directory is untouched.
|
|
outside_conn.execute("SELECT 1").fetchone()
|
|
finally:
|
|
inside.close()
|
|
reopened.close()
|
|
outside.close()
|
|
|
|
|
|
|
|
|
|
def test_stale_holder_close_does_not_evict_fresh_registry_entry(tmp_path):
|
|
"""Follow-up to #88347 — a stale holder's late ``close()`` must be inert.
|
|
|
|
After ``release_all_under`` force-closes a profile's connection, a store
|
|
re-created on the same path registers a FRESH shared entry under the same
|
|
key. If the stale holder (whose entry was force-closed) then calls
|
|
``close()``, it must not pop the fresh entry out of the registry — that
|
|
would let a third store open a SECOND connection to the same database and
|
|
silently reintroduce the multi-writer contention the registry prevents.
|
|
"""
|
|
from plugins.memory.holographic.store import MemoryStore
|
|
|
|
profile_dir = tmp_path / "profiles" / "default-2"
|
|
profile_dir.mkdir(parents=True)
|
|
db_path = profile_dir / "memory_store.db"
|
|
|
|
stale = MemoryStore(db_path=db_path, hrr_dim=64)
|
|
assert MemoryStore.release_all_under(profile_dir) == 1
|
|
|
|
fresh = MemoryStore(db_path=db_path, hrr_dim=64)
|
|
key = fresh._key
|
|
fresh_entry = MemoryStore._shared[key]
|
|
|
|
# The stale holder's late close must leave the fresh entry registered...
|
|
stale.close()
|
|
assert MemoryStore._shared.get(key) is fresh_entry
|
|
# ...and a third store must attach to the SAME shared connection.
|
|
third = MemoryStore(db_path=db_path, hrr_dim=64)
|
|
try:
|
|
assert third._conn is fresh._conn
|
|
finally:
|
|
third.close()
|
|
fresh.close()
|
|
# Normal last-holder close still evicts its own entry.
|
|
assert key not in MemoryStore._shared
|