@@ -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+
211228class 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 ]:
0 commit comments