1616import asyncio
1717import itertools
1818import os
19+ import threading
1920import time
2021import weakref
2122from abc import ABC , abstractmethod
4546TQ_STORAGE_HANDSHAKE_MAX_RETRIES = int (os .environ .get ("TQ_STORAGE_HANDSHAKE_MAX_RETRIES" , 3 ))
4647TQ_DATA_UPDATE_RESPONSE_TIMEOUT = int (os .environ .get ("TQ_DATA_UPDATE_RESPONSE_TIMEOUT" , 30 ))
4748
49+
50+ def _run_notify_loop (notify_loop : asyncio .AbstractEventLoop ) -> None :
51+ asyncio .set_event_loop (notify_loop )
52+ try :
53+ notify_loop .run_forever ()
54+ finally :
55+ notify_loop .close ()
56+
57+
4858LIMIT_THREADS_PER_MANAGER_IN_DRIVER = 8
4959LIMIT_THREADS_PER_MANAGER_IN_RAY_ACTOR = 4
5060
@@ -61,9 +71,19 @@ def __init__(self, controller_info: ZMQServerInfo, config: DictConfig):
6171 # Handshake socket is sync (used only during initialization)
6272 self .controller_handshake_socket : zmq .Socket | None = None
6373
64- self .zmq_context : zmq .asyncio .Context | None = None
74+ self .zmq_context = zmq .asyncio .Context ()
6575 self ._connect_to_controller ()
6676
77+ # Dedicated asyncio loop for ZMQ notify traffic, isolated from the caller's loop
78+ self ._notify_loop = asyncio .new_event_loop ()
79+ self ._notify_thread = threading .Thread (
80+ target = _run_notify_loop ,
81+ args = (self ._notify_loop ,),
82+ daemon = True ,
83+ name = f"{ self .storage_manager_id } -notify_data_status_loop" ,
84+ )
85+ self ._notify_thread .start ()
86+
6787 def _connect_to_controller (self ) -> None :
6888 """Initialize ZMQ sockets between storage unit and controller for handshake."""
6989 if not isinstance (self .controller_info , ZMQServerInfo ):
@@ -90,9 +110,6 @@ def _connect_to_controller(self) -> None:
90110 self .controller_handshake_socket = None
91111 sync_zmq_context .term ()
92112
93- # create async context for data status update
94- self .zmq_context = zmq .asyncio .Context ()
95-
96113 except Exception as e :
97114 logger .error (f"Failed to connect to controller: { e } " )
98115 raise
@@ -205,54 +222,51 @@ async def notify_data_update(
205222 logger .warning (f"No controller connected for storage manager { self .storage_manager_id } " )
206223 return
207224
208- # create dynamic socket
209- identity = f"{ self .storage_manager_id } -data_update-{ uuid4 ().hex [:8 ]} " .encode ()
210- sock = create_zmq_socket (self .zmq_context , zmq .DEALER , self .controller_info .ip , identity )
225+ normalized_field_schema = {}
226+ for field_name , field in field_schema .items ():
227+ field_copy = field .copy ()
228+ per_sample_shapes = field_copy .get ("per_sample_shapes" , None )
229+ if isinstance (per_sample_shapes , list | tuple ):
230+ if len (per_sample_shapes ) != len (global_indexes ):
231+ raise ValueError (
232+ f"per_sample_shapes length ({ len (per_sample_shapes )} ) does not match "
233+ f"number of global_indexes ({ len (global_indexes )} ) for field '{ field_name } '. "
234+ )
235+ field_copy ["per_sample_shapes" ] = {
236+ global_indexes [i ]: per_sample_shapes [i ] for i in range (len (global_indexes ))
237+ }
238+ normalized_field_schema [field_name ] = field_copy
211239
212- try :
213- sock .connect (self .controller_info .to_addr ("request_handle_socket" ))
214-
215- normalized_field_schema = {}
216- for field_name , field in field_schema .items ():
217- # Work on a shallow copy to avoid mutating caller-provided schema
218- field_copy = field .copy ()
219- per_sample_shapes = field_copy .get ("per_sample_shapes" , None )
220- if isinstance (per_sample_shapes , list | tuple ):
221- if len (per_sample_shapes ) != len (global_indexes ):
222- raise ValueError (
223- f"per_sample_shapes length ({ len (per_sample_shapes )} ) does not match "
224- f"number of global_indexes ({ len (global_indexes )} ) for field '{ field_name } '; "
225- f"skipping per_sample_shapes normalization."
226- )
227- else :
228- field_copy ["per_sample_shapes" ] = {
229- global_indexes [i ]: per_sample_shapes [i ] for i in range (len (global_indexes ))
230- }
231-
232- normalized_field_schema [field_name ] = field_copy
233-
234- # convert per_sample_shapes into dict
235- for field in field_schema .values ():
236- per_sample_shapes = field .get ("per_sample_shapes" , None )
237- if per_sample_shapes :
238- per_sample_shapes = {global_indexes [i ]: per_sample_shapes [i ] for i in range (len (global_indexes ))}
239- field ["per_sample_shapes" ] = per_sample_shapes
240-
241- request_msg = ZMQMessage .create (
242- request_type = ZMQRequestType .NOTIFY_DATA_UPDATE , # type: ignore[arg-type]
243- sender_id = self .storage_manager_id ,
244- body = {
245- "partition_id" : partition_id ,
246- "global_indexes" : global_indexes ,
247- "field_schema" : normalized_field_schema ,
248- "custom_backend_meta" : custom_backend_meta ,
249- },
250- ).serialize ()
240+ request_msg = ZMQMessage .create (
241+ request_type = ZMQRequestType .NOTIFY_DATA_UPDATE , # type: ignore[arg-type]
242+ sender_id = self .storage_manager_id ,
243+ body = {
244+ "partition_id" : partition_id ,
245+ "global_indexes" : global_indexes ,
246+ "field_schema" : normalized_field_schema ,
247+ "custom_backend_meta" : custom_backend_meta ,
248+ },
249+ ).serialize ()
250+
251+ thread_future = asyncio .run_coroutine_threadsafe (
252+ self ._notify_and_wait (request_msg ),
253+ self ._notify_loop ,
254+ )
255+ await asyncio .wrap_future (thread_future )
256+
257+ async def _notify_and_wait (self , request_msg : list ) -> None :
258+ """Send a data status notification to the controller and block until ACK is received."""
259+ identity = f"{ self .storage_manager_id } -notify-{ uuid4 ().hex [:8 ]} " .encode ()
260+ sock = create_zmq_socket (
261+ ctx = self .zmq_context , socket_type = zmq .DEALER , ip = self .controller_info .ip , identity = identity
262+ )
263+ sock .setsockopt (zmq .LINGER , 0 )
264+ sock .connect (self .controller_info .to_addr ("request_handle_socket" ))
251265
266+ try :
252267 await sock .send_multipart (request_msg )
253268 logger .debug (
254- f"[{ self .storage_manager_id } ]: Send data status update request "
255- f"from storage manager id #{ self .storage_manager_id } "
269+ f"[{ self .storage_manager_id } ]: Sent data status update request "
256270 f"to controller id #{ self .controller_info .id } successfully."
257271 )
258272
@@ -262,7 +276,10 @@ async def notify_data_update(
262276 while not response_received and timeout > 0 :
263277 try :
264278 poll_interval = min (TQ_STORAGE_POLLER_TIMEOUT , timeout )
265- messages = await asyncio .wait_for (sock .recv_multipart (copy = False ), timeout = poll_interval )
279+ messages = await asyncio .wait_for (
280+ sock .recv_multipart (copy = False ),
281+ timeout = poll_interval ,
282+ )
266283 response_msg = ZMQMessage .deserialize (messages )
267284
268285 if response_msg .request_type == ZMQRequestType .NOTIFY_DATA_UPDATE_ACK : # type: ignore[arg-type]
@@ -271,30 +288,22 @@ async def notify_data_update(
271288 f"[{ self .storage_manager_id } ]: Get data status update ACK response "
272289 f"from controller id #{ response_msg .sender_id } successfully."
273290 )
291+ break
274292 except asyncio .TimeoutError :
275293 timeout -= poll_interval
276294 except Exception as e :
277295 logger .warning (f"[{ self .storage_manager_id } ]: Error receiving response: { e } " )
278296 break
279297
280298 if not response_received :
281- logger .error (f"[{ self .storage_manager_id } ]: Did not receive data status update ACK." )
282-
283- except Exception as e :
284- logger .error (f"[{ self .storage_manager_id } ]: Error during notify_data_update: { e } " )
285- try :
286- error_msg = ZMQMessage .create (
287- request_type = ZMQRequestType .NOTIFY_DATA_UPDATE_ERROR , # type: ignore[arg-type]
288- sender_id = self .storage_manager_id ,
289- body = {"message" : f"Failed to notify: { str (e )} " },
290- ).serialize ()
291- await sock .send_multipart (error_msg )
292- except Exception :
293- pass
299+ logger .error (
300+ f"[{ self .storage_manager_id } ]: Timeout waiting for data status update ACK "
301+ f"from controller after { TQ_DATA_UPDATE_RESPONSE_TIMEOUT } s."
302+ )
294303 finally :
295304 try :
296305 if not sock .closed :
297- sock .close (linger = - 1 )
306+ sock .close (linger = 0 )
298307 except Exception :
299308 pass
300309
@@ -344,17 +353,26 @@ async def clear_data(self, metadata: BatchMeta) -> None:
344353 raise NotImplementedError ("Subclasses must implement clear_data" )
345354
346355 def close (self ) -> None :
347- """Close all ZMQ sockets and context to prevent resource leaks ."""
348- # Close handshake socket if it exists
356+ """Close all ZMQ sockets/contexts and stop the notify loop ."""
357+
349358 if self .controller_handshake_socket :
350359 try :
351360 if not self .controller_handshake_socket .closed :
352361 self .controller_handshake_socket .close (linger = 0 )
353362 except Exception as e :
354363 logger .error (f"[{ self .storage_manager_id } ]: Error closing controller_handshake_socket: { str (e )} " )
355364
356- if self .zmq_context :
357- self .zmq_context .term ()
365+ if hasattr (self , "_notify_loop" ) and self ._notify_loop .is_running ():
366+ self ._notify_loop .call_soon_threadsafe (self ._notify_loop .stop )
367+
368+ if hasattr (self , "_notify_thread" ) and self ._notify_thread is not None :
369+ self ._notify_thread .join (timeout = 5.0 )
370+ if self ._notify_thread .is_alive ():
371+ logger .warning (f"[{ self .storage_manager_id } ]: Notify ZMQ thread did not stop within 5 second timeout." )
372+ else :
373+ logger .debug (f"[{ self .storage_manager_id } ]: Notify ZMQ thread shut down." )
374+
375+ self .zmq_context .term ()
358376
359377 def __del__ (self ):
360378 """Destructor to ensure resources are cleaned up."""
0 commit comments