Vacuum suppresses OSError only

This commit is contained in:
Yiorgis Gozadinos 2026-07-15 16:05:24 +03:00
parent f997d7adc9
commit a1f3435df3
No known key found for this signature in database
2 changed files with 112 additions and 2 deletions

View file

@ -512,6 +512,8 @@ class Store:
Raises:
ReadOnlyError: If the store is in read-only mode.
RuntimeError: On lance errors during optimize; only OSError
(resource pressure) skips the pass.
"""
self._assert_writable()
@ -535,8 +537,10 @@ class Store:
table, retention
)
)
except (RuntimeError, OSError) as e:
# Handle resource errors gracefully
except OSError as e:
# Resource errors (e.g. disk pressure) skip the pass; lance
# errors surface as RuntimeError and must not be swallowed —
# a silently skipped cleanup hides tag-interaction bugs.
logger.debug(f"Vacuum skipped due to resource constraints: {e}")
async def _tag_safe_retention(

View file

@ -239,6 +239,112 @@ async def test_vacuum_cleans_untagged_versions_and_keeps_tagged(temp_db_path):
assert rows == 2
@pytest.mark.asyncio
async def test_vacuum_reraises_runtime_error(temp_db_path, monkeypatch):
"""Vacuum suppresses OSError only; lance errors (RuntimeError) surface
instead of silently skipping cleanup."""
from lancedb.table import AsyncTable
async with Store(temp_db_path, create=True) as store:
async def failing_optimize(self, **kwargs):
raise RuntimeError("lance error: boom")
monkeypatch.setattr(AsyncTable, "optimize", failing_optimize)
with pytest.raises(RuntimeError, match="boom"):
await store.vacuum(retention_seconds=0)
async def failing_optimize_os(self, **kwargs):
raise OSError("disk full")
monkeypatch.setattr(AsyncTable, "optimize", failing_optimize_os)
await store.vacuum(retention_seconds=0)
@pytest.mark.asyncio
async def test_vacuum_multiple_tags_uses_oldest_cutoff(temp_db_path):
"""With several tags the retention clamp must key off the oldest one;
clamping to a newer tag would put the older tagged version inside the
cleanup window and lance would hard-error."""
async with Store(temp_db_path, create=True) as store:
repo = DocumentRepository(store)
await repo.create(Document(content="First document"))
await store.create_tag("old")
await asyncio.sleep(1.5)
await repo.create(Document(content="Second document"))
await store.create_tag("new")
await store.vacuum(retention_seconds=0)
tags = await store.list_tags()
remaining = [v["version"] for v in await store.list_table_versions("documents")]
assert tags["old"].tables["documents"] in remaining
assert tags["new"].tables["documents"] in remaining
@pytest.mark.asyncio
async def test_vacuum_partial_tag_protects_its_tables(temp_db_path):
"""A partial tag still protects the versions of the tables it exists on,
while untagged tables clean up normally."""
async with Store(temp_db_path, create=True) as store:
repo = DocumentRepository(store)
await repo.create(Document(content="First document"))
chunks_version = await store.chunks_table.version()
await store.chunks_table.tags.create("stale", chunks_version)
docs_versions_before = [
v["version"] for v in await store.list_table_versions("documents")
]
await asyncio.sleep(1.5)
await repo.create(Document(content="Second document"))
await store.vacuum(retention_seconds=0)
chunk_versions = [
v["version"] for v in await store.list_table_versions("chunks")
]
assert chunks_version in chunk_versions
docs_versions_after = [
v["version"] for v in await store.list_table_versions("documents")
]
assert min(docs_versions_before) not in docs_versions_after
@pytest.mark.asyncio
async def test_deleting_oldest_tag_advances_cleanup(temp_db_path):
"""Versions pinned by a tag become cleanable once the tag is deleted;
the cleanup cutoff advances to the next retained tag without removing
its version."""
async with Store(temp_db_path, create=True) as store:
repo = DocumentRepository(store)
await repo.create(Document(content="First document"))
await store.create_tag("old")
old_version = (await store.list_tags())["old"].tables["documents"]
await asyncio.sleep(1.5)
await repo.create(Document(content="Second document"))
await store.create_tag("new")
new_version = (await store.list_tags())["new"].tables["documents"]
await store.vacuum(retention_seconds=0)
remaining = [v["version"] for v in await store.list_table_versions("documents")]
assert old_version in remaining
assert new_version in remaining
await store.delete_tag("old")
await asyncio.sleep(1.5)
await store.vacuum(retention_seconds=0)
remaining = [v["version"] for v in await store.list_table_versions("documents")]
assert old_version not in remaining
assert new_version in remaining
@pytest.mark.asyncio
async def test_tag_operations_rejected_during_rebuild(temp_db_path):
"""While a rebuild holds the rebuild lock, tag operations fail fast