fix: register async client cleanup eagerly to prevent SSL exit errors.

Closes #23278
This commit is contained in:
awais qureshi 2026-03-11 00:25:31 +05:00
parent 1103a8c620
commit 26fedb3fac
2 changed files with 28 additions and 7 deletions

View file

@ -1650,6 +1650,15 @@ if TYPE_CHECKING:
# Track if async client cleanup has been registered (for lazy loading)
_async_client_cleanup_registered = False
# Register async client cleanup eagerly so it runs even when only eagerly-imported
# symbols (e.g. `acompletion`, imported via `from .main import *`) are used.
# Without this, __getattr__ is never triggered and SSL connections are left open
# at process exit, causing "Fatal error on SSL transport" / "Event loop is closed"
# tracebacks with asyncio.gather (see issue #23278).
from litellm.llms.custom_httpx.async_client_cleanup import register_async_client_cleanup
register_async_client_cleanup()
_async_client_cleanup_registered = True
# Eager loading for backwards compatibility with VCR and other HTTP recording tools
# When LITELLM_DISABLE_LAZY_LOADING is set, lazy-loaded attributes are loaded at import time
# For now, this only affects encoding (tiktoken) as it was the only reported issue
@ -1663,13 +1672,6 @@ if os.getenv("LITELLM_DISABLE_LAZY_LOADING", "").lower() in ("1", "true", "yes",
def __getattr__(name: str) -> Any:
"""Lazy import handler with cached registry for improved performance."""
global _async_client_cleanup_registered
# Register async client cleanup on first access (only once)
if not _async_client_cleanup_registered:
from litellm.llms.custom_httpx.async_client_cleanup import register_async_client_cleanup
register_async_client_cleanup()
_async_client_cleanup_registered = True
# Use cached registry from _lazy_imports instead of importing tuples every time
from ._lazy_imports import _get_lazy_import_registry

View file

@ -118,6 +118,25 @@ async def test_atexit_cleanup():
return False
def test_async_client_cleanup_registered_at_import_time():
"""
Regression test for issue #23278: SSL transport errors with asyncio.gather.
acompletion is imported at module level via `from .main import *`, so
__getattr__ is never triggered when users call litellm.acompletion().
register_async_client_cleanup() must be called eagerly at import time,
not lazily inside __getattr__, otherwise SSL connections are left open
at process exit causing "Fatal error on SSL transport" errors.
"""
import litellm
assert litellm._async_client_cleanup_registered is True, (
"register_async_client_cleanup() must be called at import time. "
"If this fails, SSL connections will leak on process exit when "
"acompletion() is used with asyncio.gather() (issue #23278)."
)
def test_new_event_loop_atexit():
"""Test that the new atexit handler can create a fresh event loop"""
print("\n" + "=" * 70)