Merge pull request #2015 from MemPalace/agent/fix-sqlite-integrity-contention

fix(repair): wait out transient SQLite contention
This commit is contained in:
Igor Lins e Silva 2026-07-14 20:25:55 -03:00 committed by GitHub
commit 4c8ef8ed43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 1 deletions

View File

@ -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}"]

View File

@ -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()