From 38d73f52aa425a67ad79d734aa9175bcaf1a3c89 Mon Sep 17 00:00:00 2001 From: Prashant Patel Date: Wed, 6 Aug 2025 22:03:02 +0000 Subject: [PATCH 1/3] fix: properly handle async service cleanup in LocalBackend.close() The close() method was calling service.close() without checking if it was async, causing resource leaks when services had async cleanup methods. This fix: - Makes LocalBackend.close() async to match the base Backend class - Properly awaits async service close methods - Maintains backward compatibility for sync close methods - Handles __exit__ context manager compatibility Prevents GPU memory leaks and zombie processes in production deployments. --- src/art/local/backend.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/art/local/backend.py b/src/art/local/backend.py index 1129326e..64d5d27c 100644 --- a/src/art/local/backend.py +++ b/src/art/local/backend.py @@ -1,3 +1,4 @@ +import asyncio import json import math import os @@ -90,8 +91,29 @@ def __exit__( exc: BaseException | None, tb: TracebackType | None, ) -> None: + in_event_loop = True + try: + asyncio.get_running_loop() + except RuntimeError: + in_event_loop = False + if in_event_loop: + raise RuntimeError( + "LocalBackend used with 'with' inside a running event loop. Use 'async with LocalBackend()' instead." + ) + # No event loop running; safe to close synchronously self._close() + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + await self.close() + async def close(self) -> None: """ If running vLLM in a separate process, this will kill that process and close the communication threads. From 9867e8e3bfafc5051f28c8678521ffc3aa36aec2 Mon Sep 17 00:00:00 2001 From: Prashant Patel Date: Fri, 8 Aug 2025 23:37:37 -0700 Subject: [PATCH 2/3] feat(local): add async context manager support for LocalBackend and enforce async usage inside event loop\n\n- Implement __aenter__/__aexit__ to properly await cleanup\n- Make __exit__ raise if used under a running event loop, guiding to 'async with'\n- Keep sync 'with' working when no loop is running by calling asyncio.run(close()) --- src/art/local/backend.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/art/local/backend.py b/src/art/local/backend.py index 64d5d27c..c172ca6a 100644 --- a/src/art/local/backend.py +++ b/src/art/local/backend.py @@ -91,17 +91,24 @@ def __exit__( exc: BaseException | None, tb: TracebackType | None, ) -> None: - in_event_loop = True try: + # If an event loop is running, force users to use the async context manager asyncio.get_running_loop() +<<<<<<< HEAD except RuntimeError: - in_event_loop = False - if in_event_loop: - raise RuntimeError( - "LocalBackend used with 'with' inside a running event loop. Use 'async with LocalBackend()' instead." - ) - # No event loop running; safe to close synchronously - self._close() + # No event loop running; safe to close synchronously + self._close() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + await self.close() async def __aenter__(self) -> Self: return self From 0878ab4ab95d0ace123787096b062a4dd14877e9 Mon Sep 17 00:00:00 2001 From: Prashant Patel Date: Fri, 8 Aug 2025 23:42:26 -0700 Subject: [PATCH 3/3] fix(local): resolve merge conflict markers and finalize async context manager (__aenter__/__aexit__) for LocalBackend --- src/art/local/backend.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/art/local/backend.py b/src/art/local/backend.py index c172ca6a..1873f1e1 100644 --- a/src/art/local/backend.py +++ b/src/art/local/backend.py @@ -94,7 +94,9 @@ def __exit__( try: # If an event loop is running, force users to use the async context manager asyncio.get_running_loop() -<<<<<<< HEAD + raise RuntimeError( + "LocalBackend used with 'with' inside a running event loop. Use 'async with LocalBackend()' instead." + ) except RuntimeError: # No event loop running; safe to close synchronously self._close() @@ -110,17 +112,6 @@ async def __aexit__( ) -> None: await self.close() - async def __aenter__(self) -> Self: - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc: BaseException | None, - tb: TracebackType | None, - ) -> None: - await self.close() - async def close(self) -> None: """ If running vLLM in a separate process, this will kill that process and close the communication threads.