Release every sandbox resource, whichever teardown fails

Sandbox.close released the pool and the held federated client only when
the monty session's __aexit__ returned. Each release now runs under
suppress(Exception), matching _discard_session and aclose_quietly, so a
raising step neither masks an unwinding error nor leaks the databases a
federated `_opened` holds.
This commit is contained in:
Yiorgis Gozadinos 2026-08-28 15:13:28 +03:00
parent ee835b77c2
commit d0df382ce8
No known key found for this signature in database
2 changed files with 38 additions and 7 deletions

View file

@ -354,16 +354,21 @@ class Sandbox:
await session.__aexit__(None, None, None)
async def close(self) -> None:
"""Return the worker to the pool and shut the pool down. Idempotent."""
"""Return the worker to the pool, shut the pool down and release any
held connection. Idempotent, and each release happens whatever the
others raise."""
if self._session is not None:
await self._session.__aexit__(None, None, None)
self._session = None
session, self._session = self._session, None
with suppress(Exception):
await session.__aexit__(None, None, None)
if self._pool is not None:
await self._pool.__aexit__(None, None, None)
self._pool = None
pool, self._pool = self._pool, None
with suppress(Exception):
await pool.__aexit__(None, None, None)
if self._opened is not None:
await self._opened.__aexit__(None, None, None)
self._opened = None
opened, self._opened = self._opened, None
with suppress(Exception):
await opened.__aexit__(None, None, None)
def _build_external_functions(self) -> dict[str, Any]:
"""Build async external functions for the Monty interpreter."""

View file

@ -935,3 +935,29 @@ class TestSandboxRequestTimeout:
assert "alive" in recovered.stdout
finally:
await sb.close()
class TestSandboxClose:
@pytest.mark.asyncio
async def test_a_failing_teardown_still_releases_the_rest(self, tmp_path):
"""Each of the session, the pool and the held connection is released,
whichever of them fails."""
from unittest.mock import AsyncMock
sb = Sandbox(
db_path=tmp_path / "x.lancedb",
config=AppConfig(),
context=AnalysisContext(),
)
session, pool, opened = AsyncMock(), AsyncMock(), AsyncMock()
session.__aexit__.side_effect = RuntimeError("worker already gone")
sb._session, sb._pool = session, pool # ty: ignore[invalid-assignment]
sb._opened = opened
await sb.close()
pool.__aexit__.assert_awaited_once()
opened.__aexit__.assert_awaited_once()
assert sb._session is None
assert sb._pool is None
assert sb._opened is None