-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
182 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
""" | ||
LiteLLM currently have an issue where HttpHandlers are being created but not | ||
closed. We have submitted a PR to them, (https://github.com/BerriAI/litellm/pull/8711) | ||
and their dev team say they are in the process of a refactor that will fix this, but | ||
in the meantime, we need to manage the lifecycle of the httpx.Client manually. | ||
We can't simply pass in our own client object, because all the different implementations use | ||
different types of client object. | ||
So we monkey patch the httpx.Client class to track newly created instances and close these | ||
when the operations complete. (Since some paths create a single shared client and reuse these, | ||
we actually need to create a proxy object that allows these clients to be reusable.) | ||
Hopefully, this will be fixed soon and we can remove this abomination. | ||
""" | ||
|
||
import contextlib | ||
from typing import Callable | ||
|
||
import httpx | ||
|
||
|
||
@contextlib.contextmanager | ||
def ensure_httpx_close(): | ||
wrapped_class = httpx.Client | ||
proxys = [] | ||
|
||
class ClientProxy: | ||
""" | ||
Sometimes LiteLLM opens a new httpx client for each connection, and does not close them. | ||
Sometimes it does close them. Sometimes, it reuses a client between connections. For cases | ||
where a client is reused, we need to be able to reuse the client even after closing it. | ||
""" | ||
|
||
client_constructor: Callable | ||
args: tuple | ||
kwargs: dict | ||
client: httpx.Client | ||
|
||
def __init__(self, *args, **kwargs): | ||
self.args = args | ||
self.kwargs = kwargs | ||
self.client = wrapped_class(*self.args, **self.kwargs) | ||
proxys.append(self) | ||
|
||
def __getattr__(self, name): | ||
# Invoke a method on the proxied client - create one if required | ||
if self.client is None: | ||
self.client = wrapped_class(*self.args, **self.kwargs) | ||
return getattr(self.client, name) | ||
|
||
def close(self): | ||
# Close the client if it is open | ||
if self.client: | ||
self.client.close() | ||
self.client = None | ||
|
||
def __iter__(self, *args, **kwargs): | ||
# We have to override this as debuggers invoke it causing the client to reopen | ||
if self.client: | ||
return self.client.iter(*args, **kwargs) | ||
return object.__getattribute__(self, 'iter')(*args, **kwargs) | ||
|
||
@property | ||
def is_closed(self): | ||
# Check if closed | ||
if self.client is None: | ||
return True | ||
return self.client.is_closed | ||
|
||
httpx.Client = ClientProxy | ||
try: | ||
yield | ||
finally: | ||
httpx.Client = wrapped_class | ||
while proxys: | ||
proxy = proxys.pop() | ||
proxy.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import httpx | ||
|
||
from openhands.utils.ensure_httpx_close import ensure_httpx_close | ||
|
||
|
||
def test_ensure_httpx_close_basic(): | ||
"""Test basic functionality of ensure_httpx_close.""" | ||
ctx = ensure_httpx_close() | ||
with ctx: | ||
# Create a client - should be tracked | ||
client = httpx.Client() | ||
|
||
# After context exit, client should be closed | ||
assert client.is_closed | ||
|
||
|
||
def test_ensure_httpx_close_multiple_clients(): | ||
"""Test ensure_httpx_close with multiple clients.""" | ||
ctx = ensure_httpx_close() | ||
with ctx: | ||
client1 = httpx.Client() | ||
client2 = httpx.Client() | ||
|
||
assert client1.is_closed | ||
assert client2.is_closed | ||
|
||
|
||
def test_ensure_httpx_close_nested(): | ||
"""Test nested usage of ensure_httpx_close.""" | ||
with ensure_httpx_close(): | ||
client1 = httpx.Client() | ||
|
||
with ensure_httpx_close(): | ||
client2 = httpx.Client() | ||
assert not client2.is_closed | ||
|
||
# After inner context, client2 should be closed | ||
assert client2.is_closed | ||
# client1 should still be open since outer context is still active | ||
assert not client1.is_closed | ||
|
||
# After outer context, both clients should be closed | ||
assert client1.is_closed | ||
assert client2.is_closed | ||
|
||
|
||
def test_ensure_httpx_close_exception(): | ||
"""Test ensure_httpx_close when an exception occurs.""" | ||
client = None | ||
try: | ||
with ensure_httpx_close(): | ||
client = httpx.Client() | ||
raise ValueError('Test exception') | ||
except ValueError: | ||
pass | ||
|
||
# Client should be closed even if an exception occurred | ||
assert client is not None | ||
assert client.is_closed | ||
|
||
|
||
def test_ensure_httpx_close_restore_client(): | ||
"""Test that the original client is restored after context exit.""" | ||
original_client = httpx.Client | ||
with ensure_httpx_close(): | ||
assert httpx.Client != original_client | ||
|
||
# Original __init__ should be restored | ||
assert httpx.Client == original_client |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters