1717
1818log = logging .getLogger (__name__ )
1919
20+ DISPATCH_STOP_TIMEOUT = 2.0
21+ DISPATCH_QUEUE_MAXSIZE = 4096
22+
2023_FRAME_SENTINEL = object ()
2124
2225
@@ -31,7 +34,8 @@ def __init__(self):
3134 self ._lock = threading .RLock ()
3235 self ._frame_queue : queue .Queue | None = None
3336 self ._dispatch_thread : threading .Thread | None = None
34- self ._dispatch_stop = threading .Event ()
37+ self ._dispatch_accepting : bool = False
38+ self ._dispatch_sentinel_enqueued : bool = False
3539
3640 @property
3741 def is_active (self ) -> bool :
@@ -96,39 +100,79 @@ def pop(self, cam_id: str, default=None) -> VideoRecorder | None:
96100 return self ._recorders .pop (cam_id , default )
97101
98102 def _start_dispatcher (self ) -> None :
99- if self ._dispatch_thread is not None and self ._dispatch_thread .is_alive ():
100- return
103+ with self ._lock :
104+ if self ._dispatch_thread is not None and self ._dispatch_thread .is_alive ():
105+ return
101106
102- self ._dispatch_stop .clear ()
103- self ._frame_queue = queue .Queue (maxsize = 4096 )
104- self ._dispatch_thread = threading .Thread (
105- target = self ._dispatch_loop ,
106- name = "RecordingManagerDispatcher" ,
107- daemon = True ,
108- )
109- self ._dispatch_thread .start ()
107+ self ._frame_queue = queue .Queue (maxsize = DISPATCH_QUEUE_MAXSIZE )
108+ self ._dispatch_accepting = True
109+ self ._dispatch_sentinel_enqueued = False
110+ self ._dispatch_thread = threading .Thread (
111+ target = self ._dispatch_loop ,
112+ name = "RecordingManagerDispatcher" ,
113+ daemon = True ,
114+ )
115+ self ._dispatch_thread .start ()
110116
111- def _stop_dispatcher (self , timeout : float = 2.0 ) -> None :
117+ def _stop_dispatcher (
118+ self ,
119+ timeout : float = DISPATCH_STOP_TIMEOUT ,
120+ ) -> bool :
112121 with self ._lock :
113122 q = self ._frame_queue
114123 t = self ._dispatch_thread
115124
116- if q is not None :
125+ if t is None :
126+ self ._frame_queue = None
127+ self ._dispatch_accepting = False
128+ self ._dispatch_sentinel_enqueued = False
129+ return True
130+
131+ # Establish the stop boundary while holding the same lock used
132+ # by write_frame(). No new frame can be accepted after this.
133+ self ._dispatch_accepting = False
134+
135+ should_enqueue_sentinel = not self ._dispatch_sentinel_enqueued
136+ self ._dispatch_sentinel_enqueued = True
137+
138+ if should_enqueue_sentinel and q is not None :
117139 try :
118- q .put (_FRAME_SENTINEL , block = True , timeout = timeout )
140+ q .put (
141+ _FRAME_SENTINEL ,
142+ block = True ,
143+ timeout = timeout ,
144+ )
119145 except queue .Full :
120- log .warning ("Recording frame queue full while stopping dispatcher; dispatcher may not stop promptly." )
146+ # No sentinel was inserted, so allow a later stop attempt
147+ # to retry once the dispatcher has freed queue capacity.
148+ with self ._lock :
149+ if self ._dispatch_thread is t :
150+ self ._dispatch_sentinel_enqueued = False
121151
122- if t is not None :
123- t .join (timeout = timeout )
124- if t .is_alive ():
125- log .warning ("Recording frame dispatcher did not stop within %.1fs" , timeout )
152+ log .warning (
153+ "Could not enqueue recording dispatcher sentinel within %.1fs; preserving live dispatcher state" ,
154+ timeout ,
155+ )
156+ return False
157+
158+ t .join (timeout = timeout )
159+
160+ if t .is_alive ():
161+ log .warning (
162+ "Recording frame dispatcher did not stop within %.1fs; preserving its queue and thread state" ,
163+ timeout ,
164+ )
165+ return False
126166
127167 with self ._lock :
168+ # Do not clear a newer dispatcher if one was somehow started.
128169 if self ._dispatch_thread is t :
129170 self ._dispatch_thread = None
130171 self ._frame_queue = None
131- self ._dispatch_stop .clear ()
172+ self ._dispatch_accepting = False
173+ self ._dispatch_sentinel_enqueued = False
174+
175+ return True
132176
133177 def _dispatch_loop (self ) -> None :
134178 with self ._lock :
@@ -142,16 +186,30 @@ def _dispatch_loop(self) -> None:
142186
143187 try :
144188 if item is _FRAME_SENTINEL :
145- break
189+ return
190+
191+ (
192+ cam_id ,
193+ frame ,
194+ timestamp ,
195+ timestamp_metadata ,
196+ ) = item
146197
147- cam_id , frame , timestamp , timestamp_metadata = item
148- self ._write_frame_now (cam_id , frame , timestamp , timestamp_metadata )
198+ self ._write_frame_now (
199+ cam_id ,
200+ frame ,
201+ timestamp ,
202+ timestamp_metadata ,
203+ )
204+
205+ except Exception :
206+ log .exception ("Unhandled error dispatching a recording frame" )
149207
150208 finally :
151209 try :
152210 q .task_done ()
153211 except ValueError :
154- pass
212+ log . warning ( "Recording dispatcher called task_done() too many times" )
155213
156214 def start_all (
157215 self ,
@@ -257,8 +315,10 @@ def start_all(
257315
258316 return run_dir
259317
260- def stop_all (self ) -> None :
261- self ._stop_dispatcher ()
318+ def stop_all (self ) -> bool :
319+ if not self ._stop_dispatcher ():
320+ log .warning ("Recording stop is incomplete, frame dispatcher is still draining." )
321+ return False
262322
263323 with self ._lock :
264324 recorders = list (self ._recorders .items ())
@@ -275,6 +335,8 @@ def stop_all(self) -> None:
275335 self ._session_dir = None
276336 self ._run_dir = None
277337
338+ return True
339+
278340 def _write_frame_now (
279341 self , cam_id : str , frame : np .ndarray , timestamp : float | None = None , timestamp_metadata : object | None = None
280342 ) -> None :
@@ -316,28 +378,37 @@ def write_frame(
316378 timestamp : float | None = None ,
317379 timestamp_metadata : object | None = None ,
318380 ) -> None :
381+ payload = (
382+ cam_id ,
383+ frame ,
384+ timestamp if timestamp is not None else time .time (),
385+ timestamp_metadata ,
386+ )
387+
319388 with self ._lock :
320- active = cam_id in self ._recorders
321- if not active :
389+ if cam_id not in self ._recorders :
322390 return
323391
324392 if self ._frame_queue is None or self ._dispatch_thread is None or not self ._dispatch_thread .is_alive ():
325393 self ._start_dispatcher ()
326394
327395 q = self ._frame_queue
328396
329- if q is None :
330- return
397+ if q is None or not self . _dispatch_accepting :
398+ return
331399
332- try :
333- q .put_nowait ((cam_id , frame , timestamp if timestamp is not None else time .time (), timestamp_metadata ))
334- except queue .Full :
335- log .warning (
336- "Recording manager frame queue full; dropping frame for %s. frame_shape=%s dtype=%s" ,
337- cam_id ,
338- getattr (frame , "shape" , None ),
339- getattr (frame , "dtype" , None ),
340- )
400+ try :
401+ q .put_nowait (payload )
402+ return
403+ except queue .Full :
404+ pass
405+
406+ log .warning (
407+ "Recording manager frame queue full; dropping frame for %s. frame_shape=%s dtype=%s" ,
408+ cam_id ,
409+ getattr (frame , "shape" , None ),
410+ getattr (frame , "dtype" , None ),
411+ )
341412
342413 def flush (self , timeout : float = 2.0 ) -> bool :
343414 """Wait until all currently queued recording frames have been dispatched.
0 commit comments