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:
parent
ee835b77c2
commit
d0df382ce8
2 changed files with 38 additions and 7 deletions
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue