Skip to content

Commit 4fd3472

Browse files
authored
fix(lancedb): treat a lance spill failure as retryable
Squash merge of PR #463.
1 parent 401fbf8 commit 4fd3472

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

‎src/everos/core/persistence/lancedb/repository.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,23 @@ def _remove_empty_index_dirs(
208208
return removed
209209

210210

211+
_TRANSIENT_EXECUTION_MARKERS = ("Spill has sent an error",)
212+
"""Substrings of lance error messages that name a query-execution failure a
213+
retry clears. ``LanceError(IO): Execution error: Spill has sent an error`` is
214+
DataFusion's sort / merge spill to the OS temp dir failing mid-query; lancedb
215+
raises it as a bare ``RuntimeError``. Seen only on the Windows soak box under
216+
nine concurrent clients (187 / 180 / 34 times over three runs), never on an
217+
idle box, and the same row projected fine on the next attempt — yet the worker
218+
filed every one as unrecoverable, so ~200 md files per run needed a manual
219+
``cascade fix``. Match the exact phrase: a generic IO error (disk full, file
220+
gone) must stay permanent."""
221+
222+
223+
def _is_transient_execution_error(exc: BaseException) -> bool:
224+
text = str(exc)
225+
return any(marker in text for marker in _TRANSIENT_EXECUTION_MARKERS)
226+
227+
211228
class LanceRepoBase[T: BaseLanceTable]:
212229
"""Generic CRUD repository for one LanceDB table.
213230
@@ -296,6 +313,19 @@ async def _deadline(self, budget: float, op: str) -> AsyncIterator[None]:
296313
raise VectorStoreBusyError(
297314
f"{op} on table {self.table_name!r} exceeded its {budget:g}s deadline"
298315
) from exc
316+
except RuntimeError as exc:
317+
if not _is_transient_execution_error(exc):
318+
raise
319+
logger.warning(
320+
"lancedb_transient_execution_error",
321+
table=self.table_name,
322+
op=op,
323+
error=str(exc)[:200],
324+
)
325+
raise VectorStoreBusyError(
326+
f"{op} on table {self.table_name!r} hit a transient lance "
327+
f"execution error: {exc}"
328+
) from exc
299329

300330
@asynccontextmanager
301331
async def _locked(self, budget: float, op: str) -> AsyncIterator[None]:
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""A lance query-execution failure that a retry clears must reach the cascade
2+
worker as :class:`VectorStoreBusyError` (retried with backoff), not as a bare
3+
``RuntimeError`` (filed as unrecoverable, needing a manual ``cascade fix``).
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from typing import ClassVar
9+
10+
import pytest
11+
12+
from everos.core.errors import VectorStoreBusyError
13+
from everos.core.persistence.lancedb import BaseLanceTable, LanceRepoBase
14+
from everos.core.persistence.lancedb import repository as repo_mod
15+
16+
_SPILL = (
17+
"lance error: LanceError(IO): Execution error: Spill has sent an error, "
18+
"C:\\Users\\x\\everos-src\\.venv\\Lib\\site-packages\\lance\\..."
19+
)
20+
21+
22+
class _Note(BaseLanceTable):
23+
TABLE_NAME: ClassVar[str] = "_note"
24+
id: str
25+
26+
27+
class _Repo(LanceRepoBase[_Note]):
28+
schema = _Note
29+
30+
31+
def test_only_the_spill_phrase_counts_as_transient() -> None:
32+
assert repo_mod._is_transient_execution_error(RuntimeError(_SPILL))
33+
assert not repo_mod._is_transient_execution_error(
34+
RuntimeError("lance error: LanceError(IO): No space left on device")
35+
)
36+
37+
38+
async def test_spill_failure_inside_a_read_becomes_a_busy_error() -> None:
39+
with pytest.raises(VectorStoreBusyError, match="transient lance execution"):
40+
async with _Repo()._deadline(1.0, "find_where"):
41+
raise RuntimeError(_SPILL)
42+
43+
44+
async def test_other_runtime_errors_still_propagate_unchanged() -> None:
45+
with pytest.raises(RuntimeError, match="No space left"):
46+
async with _Repo()._deadline(1.0, "find_where"):
47+
raise RuntimeError("lance error: LanceError(IO): No space left on device")

0 commit comments

Comments
 (0)