77"""
88
99import asyncio
10+ import atexit
1011import logging
1112import threading
1213import time
@@ -225,6 +226,10 @@ def _cleanup_idle_connections(self):
225226 """Background thread to clean up idle connections."""
226227 while not self ._shutdown :
227228 try :
229+ # Check if Python is shutting down
230+ if hasattr (threading , 'main_thread' ) and not threading .main_thread ().is_alive ():
231+ break
232+
228233 current_time = time .time ()
229234 for adapter_type , pool in self ._pools .items ():
230235 temp_connections = []
@@ -241,13 +246,14 @@ def _cleanup_idle_connections(self):
241246 if hasattr (pooled_conn .connection , 'close' ):
242247 if asyncio .iscoroutinefunction (pooled_conn .connection .close ):
243248 # For async close methods, create a new event loop in this thread
244- import asyncio
245- close_loop = asyncio .new_event_loop ()
246- asyncio .set_event_loop (close_loop )
247- try :
248- close_loop .run_until_complete (pooled_conn .connection .close ())
249- finally :
250- close_loop .close ()
249+ # But only if Python is not shutting down
250+ if not (hasattr (threading , 'main_thread' ) and not threading .main_thread ().is_alive ()):
251+ close_loop = asyncio .new_event_loop ()
252+ asyncio .set_event_loop (close_loop )
253+ try :
254+ close_loop .run_until_complete (pooled_conn .connection .close ())
255+ finally :
256+ close_loop .close ()
251257 else :
252258 pooled_conn .connection .close ()
253259 logger .info (f"Cleaned up idle { adapter_type } connection" )
@@ -292,15 +298,30 @@ async def cleanup_all(self):
292298
293299 def close_all_pools (self ):
294300 """Synchronous method to close all connection pools."""
295- import asyncio
296301 try :
297- # Run the async cleanup in a new event loop if needed
298- loop = asyncio .new_event_loop ()
299- asyncio .set_event_loop (loop )
302+ # Check if there's already an event loop running
300303 try :
301- loop .run_until_complete (self .cleanup_all ())
302- finally :
303- loop .close ()
304+ loop = asyncio .get_running_loop ()
305+ # If there's a running loop, we need to run the cleanup synchronously
306+ # since close_all_pools is a synchronous method
307+ # We'll use run_coroutine_threadsafe to run it in the existing loop
308+ import concurrent .futures
309+ future = asyncio .run_coroutine_threadsafe (self .cleanup_all (), loop )
310+ # Wait for completion with timeout
311+ try :
312+ future .result (timeout = 10 ) # 10 second timeout
313+ logger .info ("Cleanup completed in running event loop" )
314+ except concurrent .futures .TimeoutError :
315+ logger .warning ("Cleanup timed out" )
316+ future .cancel ()
317+ except RuntimeError :
318+ # No running loop, we can create our own
319+ new_loop = asyncio .new_event_loop ()
320+ asyncio .set_event_loop (new_loop )
321+ try :
322+ new_loop .run_until_complete (self .cleanup_all ())
323+ finally :
324+ new_loop .close ()
304325 except Exception as e :
305326 logger .error (f"Error closing all pools: { e } " )
306327 # Force cleanup even if there's an error
@@ -347,11 +368,50 @@ def get_pool_stats(self) -> Dict[str, Dict[str, Any]]:
347368 'available_connections' : pool .qsize ()
348369 }
349370 return stats
371+
372+ def __del__ (self ):
373+ """Cleanup when the object is garbage collected."""
374+ try :
375+ # Set shutdown flag to prevent any further operations
376+ self ._shutdown = True
377+
378+ # Stop the cleanup thread if it's running
379+ if hasattr (self , '_cleanup_thread' ) and self ._cleanup_thread and self ._cleanup_thread .is_alive ():
380+ self ._cleanup_thread .join (timeout = 0.1 ) # Very short timeout during GC
381+
382+ # Clear all pools to prevent any further operations
383+ if hasattr (self , '_pools' ):
384+ self ._pools .clear ()
385+ if hasattr (self , '_connection_count' ):
386+ self ._connection_count .clear ()
387+
388+ # Replace cleanup_all with a no-op to prevent any async calls during GC
389+ async def _noop_cleanup ():
390+ return None
391+ self .cleanup_all = _noop_cleanup
392+
393+ except Exception :
394+ # Ignore any errors during garbage collection
395+ pass
350396
351397# Global singleton instance
352398_connection_pool_manager : Optional [ConnectionPoolManager ] = None
353399_pool_lock = threading .Lock ()
354400
401+ # Register cleanup function to be called at exit
402+ def _cleanup_at_exit ():
403+ """Cleanup function to be called at Python exit."""
404+ global _connection_pool_manager
405+ if _connection_pool_manager is not None :
406+ try :
407+ _connection_pool_manager ._shutdown = True
408+ if _connection_pool_manager ._cleanup_thread and _connection_pool_manager ._cleanup_thread .is_alive ():
409+ _connection_pool_manager ._cleanup_thread .join (timeout = 0.5 )
410+ except Exception :
411+ pass # Ignore errors during exit
412+
413+ atexit .register (_cleanup_at_exit )
414+
355415def get_connection_pool_manager () -> ConnectionPoolManager :
356416 """Get the global connection pool manager instance.
357417
0 commit comments