Merge pull request #2015 from MemPalace/agent/fix-sqlite-integrity-contention
fix(repair): wait out transient SQLite contention
This commit is contained in:
commit
4c8ef8ed43
|
|
@ -573,6 +573,9 @@ def sqlite_drawer_count(palace_path: str, collection_name: Optional[str] = None)
|
|||
return None
|
||||
|
||||
|
||||
_SQLITE_INTEGRITY_BUSY_TIMEOUT_SECONDS = 15.0
|
||||
|
||||
|
||||
def sqlite_integrity_errors(palace_path: str) -> list[str]:
|
||||
"""Return SQLite quick_check errors for chroma.sqlite3.
|
||||
|
||||
|
|
@ -591,7 +594,16 @@ def sqlite_integrity_errors(palace_path: str) -> list[str]:
|
|||
return []
|
||||
|
||||
try:
|
||||
with sqlite3.connect(sqlite_read_uri(sqlite_path), uri=True) as conn:
|
||||
# A writer holding SQLite's lock is contention, not corruption. The
|
||||
# sqlite3 module defaults to five seconds, which is shorter than
|
||||
# routine batch mines and curator writes on rollback-journal palaces.
|
||||
# Give those writers a bounded grace period before surfacing BUSY to
|
||||
# callers; genuine corruption still comes from PRAGMA quick_check.
|
||||
with sqlite3.connect(
|
||||
sqlite_read_uri(sqlite_path),
|
||||
uri=True,
|
||||
timeout=_SQLITE_INTEGRITY_BUSY_TIMEOUT_SECONDS,
|
||||
) as conn:
|
||||
rows = conn.execute("PRAGMA quick_check").fetchall()
|
||||
except sqlite3.Error as e:
|
||||
return [f"PRAGMA quick_check failed: {e}"]
|
||||
|
|
|
|||
|
|
@ -1358,6 +1358,57 @@ def test_sqlite_integrity_errors_returns_empty_for_healthy_db(tmp_path):
|
|||
assert repair.sqlite_integrity_errors(str(palace)) == []
|
||||
|
||||
|
||||
def test_sqlite_integrity_errors_uses_bounded_contention_timeout(tmp_path, monkeypatch):
|
||||
"""Integrity checks wait out routine writers without a real-time sleep.
|
||||
|
||||
Assert the sqlite connection contract directly so this regression test is
|
||||
deterministic and does not add the seven-second delay from the original
|
||||
proposal to every test run.
|
||||
"""
|
||||
palace = tmp_path / "palace"
|
||||
palace.mkdir()
|
||||
db_path = palace / "chroma.sqlite3"
|
||||
db_path.touch()
|
||||
|
||||
calls = []
|
||||
|
||||
class _Result:
|
||||
@staticmethod
|
||||
def fetchall():
|
||||
return [("ok",)]
|
||||
|
||||
class _Connection:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def execute(self, statement):
|
||||
calls.append(("execute", statement))
|
||||
return _Result()
|
||||
|
||||
def _connect(database, **kwargs):
|
||||
calls.append(("connect", database, kwargs))
|
||||
return _Connection()
|
||||
|
||||
monkeypatch.setattr(repair.sqlite3, "connect", _connect)
|
||||
|
||||
assert repair.sqlite_integrity_errors(str(palace)) == []
|
||||
assert calls == [
|
||||
(
|
||||
"connect",
|
||||
repair.sqlite_read_uri(str(db_path)),
|
||||
{
|
||||
"uri": True,
|
||||
"timeout": repair._SQLITE_INTEGRITY_BUSY_TIMEOUT_SECONDS,
|
||||
},
|
||||
),
|
||||
("execute", "PRAGMA quick_check"),
|
||||
]
|
||||
assert repair._SQLITE_INTEGRITY_BUSY_TIMEOUT_SECONDS == 15.0
|
||||
|
||||
|
||||
def test_sqlite_integrity_errors_reports_unreadable_sqlite_file(tmp_path):
|
||||
palace = tmp_path / "palace"
|
||||
palace.mkdir()
|
||||
|
|
|
|||
Loading…
Reference in New Issue