fix(python): avoid async work in sync reprs (#3620)

## Summary

- keep the existing synchronous `connect()` path unchanged
- make `LanceDBConnection.__repr__` and `LanceTable.__repr__`
side-effect-free
- add a regression test that verifies sync reprs do not call the Python
background loop

## Root cause

The freeze is caused by debugger rendering, not by `connect()` itself:

1. debugpy stops at a breakpoint and suspends all Python threads.
2. The debugger renders the new `db_connection` local by calling
`repr()`.
3. `LanceDBConnection.__repr__` reads `read_consistency_interval`.
4. That property calls `LOOP.run(...).result()`.
5. The `LanceDBBackgroundEventLoop` thread is suspended by the debugger,
so `repr()` waits for a thread that cannot run.

This explains why the symptom appears immediately after `connect()`: it
is the first point where a connection object exists in locals and is
automatically rendered. `LanceTable.__repr__` had the same problem
because it also read the connection's consistency interval.

This follows the same principle as #3411: `__repr__` must not trigger
async work or I/O that a debugger assumes is lightweight.

## Evidence

I reproduced the behavior with the real LanceDB classes and debugpy
1.8.21 using a DAP client:

- latest `main` (`ff6ff099`): the debugger reported `allThreadsStopped:
true`, and evaluating `repr(db_connection)` timed out
- this branch (`5755a5ba`): the same evaluation returned
`LanceDBConnection(uri='/tmp/lancedb-debug-repro')` immediately
- setting `PYDEVD_UNBLOCK_THREADS_TIMEOUT=0` also allowed the original
repr path to complete, independently confirming that it was waiting on a
suspended thread

The regression test creates a connection and table, replaces `LOOP.run`
with a function that fails, and verifies that both reprs still work.

## Validation

- `maturin develop --manifest-path python/Cargo.toml`
- `python -m pytest
python/python/tests/test_db.py::test_sync_repr_does_not_use_background_loop
python/python/tests/test_table.py::test_consistency -q` (`4 passed`)
- `ruff check .`
- `ruff format --check python/python/lancedb/db.py
python/python/lancedb/table.py python/python/tests/test_db.py
python/python/tests/test_table.py`
- `git diff --check`

Refs #3611.
This commit is contained in:
heart
2026-07-30 22:56:13 +08:00
committed by GitHub
parent 1ad6ce3a4e
commit 4dc2d9a0f2
4 changed files with 17 additions and 15 deletions
+1 -5
View File
@@ -688,11 +688,7 @@ class LanceDBConnection(DBConnection):
return cls(None, _inner=inner)
def __repr__(self) -> str:
val = f"{self.__class__.__name__}(uri={self._conn.uri!r}"
if self.read_consistency_interval is not None:
val += f", read_consistency_interval={repr(self.read_consistency_interval)}"
val += ")"
return val
return f"{self.__class__.__name__}(uri={self._conn.uri!r})"
@override
def serialize(self) -> str:
+1 -7
View File
@@ -2468,13 +2468,7 @@ class LanceTable(Table):
return LOOP.run(self._table.count_rows(filter))
def __repr__(self) -> str:
val = f"{self.__class__.__name__}(name={self.name!r}"
if self._conn.read_consistency_interval is not None:
val += ", read_consistency_interval={!r}".format(
self._conn.read_consistency_interval
)
val += f", _conn={self._conn!r})"
return val
return f"{self.__class__.__name__}(name={self.name!r}, _conn={self._conn!r})"
def __str__(self) -> str:
return self.__repr__()
+15
View File
@@ -62,6 +62,21 @@ def test_basic(tmp_path):
assert db.open_table("test").name == db["test"].name
def test_sync_repr_does_not_use_background_loop(tmp_path, monkeypatch):
from lancedb.background_loop import LOOP
db = lancedb.connect(tmp_path)
table = db.create_table("test", data=[{"id": 1}])
def fail_run(*args, **kwargs):
raise AssertionError("repr should not use the Python background loop")
monkeypatch.setattr(LOOP, "run", fail_run)
assert repr(db) == f"LanceDBConnection(uri={str(tmp_path)!r})"
assert repr(table) == f"LanceTable(name='test', _conn={db!r})"
def test_ingest_pd(tmp_path):
db = lancedb.connect(tmp_path)
-3
View File
@@ -3087,9 +3087,6 @@ def test_consistency(tmp_path, consistency_interval):
db2 = lancedb.connect(tmp_path, read_consistency_interval=consistency_interval)
table2 = db2.open_table("my_table")
if consistency_interval is not None:
assert "read_consistency_interval=datetime.timedelta(" in repr(db2)
assert "read_consistency_interval=datetime.timedelta(" in repr(table2)
assert table2.version == table.version
table.add([{"id": 1}])