fix(state): VACUUM is gated by the same quarantine rule as the checkpoints
Follow-up to #106315. vacuum() ran PRAGMA wal_checkpoint + VACUUM + wal_checkpoint(TRUNCATE) on self._conn with no quarantine check; the only guard it inherited (optimize_fts raising DeletedWalGenerationError) was swallowed by its own try/except and the rewrite proceeded on the split-brain handle. Mutation on main: vacuum() returned 2 and rewrote pages after the write stop.
This commit is contained in:
@@ -336,7 +336,14 @@ class SessionMaintenanceMixin:
|
||||
"""VACUUM to reclaim space after large deletes (SQLite never shrinks on its own).
|
||||
Takes an exclusive lock — callers must ensure no other writers are active. FTS5
|
||||
segments are merged first (:meth:`optimize_fts`) so their pages are reclaimed too;
|
||||
returns the number of FTS indexes optimized (0 on merge failure / no FTS)."""
|
||||
returns the number of FTS indexes optimized (0 on merge failure / no FTS). A quarantined
|
||||
handle (corrupt image, replaced file, lost WAL generation) never checkpoints or rewrites
|
||||
pages: the halt raised by ``optimize_fts`` would otherwise be swallowed below and the
|
||||
VACUUM would proceed on the split-brain handle (#105670)."""
|
||||
quarantine_reason = self._quarantine_reason()
|
||||
if quarantine_reason is not None:
|
||||
logger.warning("Skipping VACUUM for %s: this handle observed %s.", self.db_path, quarantine_reason)
|
||||
return 0
|
||||
optimized = 0
|
||||
try:
|
||||
optimized = self.optimize_fts() # manages its own lock
|
||||
|
||||
@@ -62,7 +62,7 @@ def _unlink_sidecars(db_path: Path) -> None:
|
||||
|
||||
@pytest.mark.linux_only # deleted-WAL write halt uses Linux unlink semantics
|
||||
def test_close_after_halt_runs_no_checkpoint(tmp_path, force_wal):
|
||||
"""A writer halted by DeletedWalGenerationError must not checkpoint (periodic or close) nor run FTS repair."""
|
||||
"""A writer halted by DeletedWalGenerationError must not checkpoint (periodic, VACUUM or close) nor run FTS repair."""
|
||||
path = tmp_path / "state.db"
|
||||
db = _make_db(path, "s", "before")
|
||||
_require_wal(db)
|
||||
@@ -72,11 +72,13 @@ def test_close_after_halt_runs_no_checkpoint(tmp_path, force_wal):
|
||||
db.append_message("s", role="user", content="after-unlink")
|
||||
assert db._db_wal_generation_lost is True
|
||||
|
||||
# Neither the periodic checkpoint, the stale-FTS retry, nor close() may touch the file now.
|
||||
# Neither the periodic checkpoint, the stale-FTS retry, VACUUM, nor close() may touch the file now.
|
||||
with patch.object(db._conn, "execute", wraps=db._conn.execute) as mock_execute:
|
||||
db._try_wal_checkpoint()
|
||||
db._fts_stale = True
|
||||
assert db.retry_deferred_fts_recovery() is False
|
||||
assert db.vacuum() == 0
|
||||
assert not [c for c in mock_execute.call_args_list if "vacuum" in str(c).lower()], "VACUUM ran on a quarantined handle"
|
||||
db.close()
|
||||
# No checkpoint call should have been made.
|
||||
checkpoint_calls = [
|
||||
|
||||
Reference in New Issue
Block a user