55import time
66import traceback
77from concurrent .futures import ThreadPoolExecutor , as_completed
8+ from typing import List , Optional , Any
89
910from conductor .client .configuration .configuration import Configuration
1011from conductor .client .configuration .settings .metrics_settings import MetricsSettings
@@ -90,6 +91,7 @@ def __init__(
9091 self ._max_workers = max_workers
9192 self ._last_poll_time = 0 # Track last poll to avoid excessive polling when queue is empty
9293 self ._consecutive_empty_polls = 0 # Track empty polls to implement backoff
94+ self ._shutdown = False # Flag to indicate graceful shutdown
9395
9496 def run (self ) -> None :
9597 if self .configuration is not None :
@@ -114,8 +116,53 @@ def run(self) -> None:
114116 self .worker .get_polling_interval_in_seconds ()
115117 )
116118
117- while True :
118- self .run_once ()
119+ try :
120+ while not self ._shutdown :
121+ self .run_once ()
122+ finally :
123+ # Cleanup resources on exit
124+ self ._cleanup ()
125+
126+ def stop (self ) -> None :
127+ """Signal the runner to stop gracefully."""
128+ self ._shutdown = True
129+
130+ def _cleanup (self ) -> None :
131+ """Clean up resources - called on exit."""
132+ logger .debug ("Cleaning up TaskRunner resources..." )
133+
134+ # Shutdown ThreadPoolExecutor (EAFP style - more Pythonic)
135+ try :
136+ self ._executor .shutdown (wait = True , cancel_futures = True )
137+ logger .debug ("ThreadPoolExecutor shut down successfully" )
138+ except AttributeError :
139+ pass # No executor to shutdown
140+ except (RuntimeError , ValueError ) as e :
141+ logger .warning (f"Error shutting down executor: { e } " )
142+
143+ # Close HTTP client (EAFP style)
144+ try :
145+ rest_client = self .task_client .api_client .rest_client
146+ rest_client .close ()
147+ logger .debug ("HTTP client closed successfully" )
148+ except AttributeError :
149+ pass # No client to close or no close method
150+ except (IOError , OSError ) as e :
151+ logger .warning (f"Error closing HTTP client: { e } " )
152+
153+ # Clear event listeners
154+ self .event_dispatcher = None
155+
156+ logger .debug ("TaskRunner cleanup completed" )
157+
158+ def __enter__ (self ):
159+ """Context manager entry - returns self for 'with' statement usage."""
160+ return self
161+
162+ def __exit__ (self , exc_type , exc_val , exc_tb ):
163+ """Context manager exit - ensures cleanup even if exception occurs."""
164+ self ._cleanup ()
165+ return False # Don't suppress exceptions
119166
120167 def __register_task_definition (self ) -> None :
121168 """
@@ -350,8 +397,11 @@ def run_once(self) -> None:
350397 self .__cleanup_completed_tasks ()
351398
352399 # Check if we can accept more tasks (based on thread_count)
353- # Account for pending async tasks in capacity calculation
354- pending_async_count = len (getattr (self .worker , '_pending_async_tasks' , {}))
400+ # Account for pending async tasks in capacity calculation (thread-safe)
401+ pending_async_count = 0
402+ if hasattr (self .worker , '_pending_tasks_lock' ) and hasattr (self .worker , '_pending_async_tasks' ):
403+ with self .worker ._pending_tasks_lock :
404+ pending_async_count = len (self .worker ._pending_async_tasks )
355405 current_capacity = len (self ._running_tasks ) + pending_async_count
356406 if current_capacity >= self ._max_workers :
357407 # At capacity - sleep briefly then return to check again
@@ -397,9 +447,11 @@ def run_once(self) -> None:
397447 logger .error ("Error in run_once: %s" , traceback .format_exc ())
398448
399449 def __cleanup_completed_tasks (self ) -> None :
400- """Remove completed task futures from tracking set"""
401- # Fast path: use difference_update for better performance
402- self ._running_tasks = {f for f in self ._running_tasks if not f .done ()}
450+ """Remove completed task futures from tracking set (thread-safe)"""
451+ # Avoid recreating the set - modify in place to prevent race conditions
452+ completed = [f for f in self ._running_tasks if f .done ()]
453+ for f in completed :
454+ self ._running_tasks .discard (f )
403455
404456 def __check_completed_async_tasks (self ) -> None :
405457 """Check for completed async tasks and update Conductor"""
0 commit comments