77
88import functools
99import threading
10- import time
1110from logging import getLogger
1211from os import getenv
1312from queue import Queue
@@ -264,18 +263,20 @@ class RelayClientManager(BaseRemoteConfiguredJobClientManager):
264263 """
265264 status_cache : Dict [str , Any ]
266265
267- def __init__ (self , relay_url : str , relay_username : str , relay_password : str , ** kwds : Dict [str , Any ]):
266+ def __init__ (self , relay_url : str , relay_username : str , relay_password : str , relay_topic_prefix : str = '' , ** kwds : Dict [str , Any ]):
268267 super ().__init__ (** kwds )
269268
270269 if not relay_url :
271270 raise Exception ("relay_url is required for RelayClientManager" )
272271
273272 # Initialize relay transport
274273 self .relay_transport = RelayTransport (relay_url , relay_username , relay_password )
274+ self .relay_topic_prefix = relay_topic_prefix
275275 self .status_cache = {}
276276 self .callback_lock = threading .Lock ()
277277 self .callback_thread = None
278278 self .active = True
279+ self .shutdown_event = threading .Event ()
279280
280281 def callback_wrapper (self , callback , message_data ):
281282 """Process status update messages from the relay."""
@@ -298,7 +299,7 @@ def callback_wrapper(self, callback, message_data):
298299 def status_consumer (self , callback_wrapper ):
299300 """Long-poll the relay for status update messages."""
300301 manager_name = self .manager_name
301- topic = f"job_status_update_ { manager_name } " if manager_name != "_default_" else "job_status_update"
302+ topic = self . _make_topic_name ( "job_status_update" , manager_name )
302303
303304 log .info ("Starting relay status consumer for topic '%s'" , topic )
304305
@@ -314,12 +315,14 @@ def status_consumer(self, callback_wrapper):
314315 if self .active :
315316 log .exception ("Exception while polling for status updates from relay, will retry." )
316317 # Brief sleep before retrying to avoid tight loop on persistent errors
317- time .sleep (5 )
318+ # Use wait() instead of sleep() to allow immediate interruption on shutdown
319+ if self .shutdown_event .wait (timeout = 5 ):
320+ break
318321 else :
319322 log .debug ("Exception during shutdown, ignoring." )
320323 break
321324
322- log .debug ( "Leaving Pulsar client relay status consumer, no additional updates will be processed." )
325+ log .info ( "Done consuming relay status updates for topic %s" , topic )
323326
324327 def ensure_has_status_update_callback (self , callback ):
325328 """Start a thread to poll for status updates if not already running."""
@@ -333,17 +336,47 @@ def ensure_has_status_update_callback(self, callback):
333336 name = "pulsar_client_%s_relay_status_consumer" % self .manager_name ,
334337 target = run
335338 )
336- thread .daemon = False # Don't interrupt processing
339+ # Make daemon so Python can exit even if thread is blocked in HTTP request.
340+ # Unlike MessageQueueClientManager which uses AMQP connections that can be
341+ # interrupted cleanly, HTTP long-poll requests block until timeout.
342+ thread .daemon = True
337343 thread .start ()
338344 self .callback_thread = thread
339345
340346 def ensure_has_ack_consumers (self ):
341347 """No-op for relay client manager, as acknowledgements are handled via HTTP."""
342348 pass
343349
350+ def _make_topic_name (self , base_topic : str , manager_name : str ) -> str :
351+ """Create a topic name with optional prefix and manager suffix.
352+
353+ Args:
354+ base_topic: Base topic name (e.g., 'job_setup', 'job_status_update')
355+ manager_name: Manager name (e.g., '_default_', 'cluster_a')
356+
357+ Returns:
358+ Fully qualified topic name
359+ """
360+ parts = []
361+
362+ # Add prefix if provided
363+ if self .relay_topic_prefix :
364+ parts .append (self .relay_topic_prefix )
365+
366+ # Add base topic
367+ parts .append (base_topic )
368+
369+ # Add manager name if not default
370+ if manager_name != "_default_" :
371+ parts .append (manager_name )
372+
373+ return "_" .join (parts )
374+
344375 def shutdown (self , ensure_cleanup : bool = False ):
345376 """Shutdown the client manager and cleanup resources."""
346377 self .active = False
378+ # Signal the shutdown event to interrupt any waiting threads
379+ self .shutdown_event .set ()
347380 if ensure_cleanup :
348381 if self .callback_thread is not None :
349382 self .callback_thread .join ()
@@ -391,6 +424,7 @@ def build_client_manager(
391424 relay_url : Optional [str ] = None ,
392425 relay_username : Optional [str ] = None ,
393426 relay_password : Optional [str ] = None ,
427+ relay_topic_prefix : Optional [str ] = None ,
394428 amqp_url : Optional [str ] = None ,
395429 k8s_enabled : Optional [bool ] = None ,
396430 tes_enabled : Optional [bool ] = None ,
@@ -401,7 +435,13 @@ def build_client_manager(
401435 return ClientManager (job_manager = job_manager , ** kwargs ) # TODO: Consider more separation here.
402436 elif relay_url :
403437 assert relay_password and relay_username , "relay_url set, but relay_username and relay_password must also be set"
404- return RelayClientManager (relay_url = relay_url , relay_username = relay_username , relay_password = relay_password , ** kwargs )
438+ return RelayClientManager (
439+ relay_url = relay_url ,
440+ relay_username = relay_username ,
441+ relay_password = relay_password ,
442+ relay_topic_prefix = relay_topic_prefix or '' ,
443+ ** kwargs
444+ )
405445 elif amqp_url :
406446 return MessageQueueClientManager (amqp_url = amqp_url , ** kwargs )
407447 elif k8s_enabled or tes_enabled or gcp_batch_enabled :
0 commit comments