fix(http): free short-lived event loops whose handler finalizer close finished as the loop stopped

This commit is contained in:
mateo-berri 2026-10-28 19:02:54 -07:00
parent 438ad98928
commit e5c0ff3294
2 changed files with 26 additions and 1 deletions

View file

@ -1119,6 +1119,15 @@ class AsyncHTTPHandler:
# Mirrors LiteLLMAiohttpTransport._background_close_tasks.
_finalizer_close_tasks: ClassVar[set["asyncio.Task[None]"]] = set() # mutable-ok: strong refs for pending closes
@classmethod
async def _close_client_at_finalization(cls, client: httpx.AsyncClient) -> None:
try:
await client.aclose()
finally:
task: Final = asyncio.current_task()
if task is not None:
cls._finalizer_close_tasks.discard(task)
@classmethod
def _on_finalizer_close_done(cls, task: "asyncio.Task[None]") -> None:
cls._finalizer_close_tasks.discard(task)
@ -1186,8 +1195,8 @@ class AsyncHTTPHandler:
# here is the cross-loop path the transport refuses.
self._dispose_wrapped_aiohttp_session()
return
task: Final = loop.create_task(self._client.aclose())
cls: Final = type(self)
task: Final = loop.create_task(cls._close_client_at_finalization(self._client))
cls._finalizer_close_tasks.add(task)
task.add_done_callback(cls._on_finalizer_close_done)
except Exception:

View file

@ -1388,6 +1388,22 @@ async def test_finalizer_with_running_loop_schedules_close_and_holds_task_ref():
assert not (AsyncHTTPHandler._finalizer_close_tasks & scheduled)
async def _drop_unused_handler() -> None:
AsyncHTTPHandler(timeout=61.0)
gc.collect()
def test_finalizer_close_does_not_keep_a_loop_closed_right_after_it_alive():
loop: Final = asyncio.new_event_loop()
loop.run_until_complete(_drop_unused_handler())
loop.close()
closed_loop: Final = weakref.ref(loop)
del loop
gc.collect()
assert closed_loop() is None
@pytest.mark.asyncio
async def test_sync_close_helper_respects_session_ownership():
"""The loop-less fallback closes only sessions the transport owns; a