Skip to content

Commit 3b5ba55

Browse files
committed
Fix dispatcher lifecycle and add flush API
Refactors recording frame dispatching to start lazily in `write_frame`, simplifies the dispatch loop to block on queue reads, and makes dispatcher shutdown more reliable by waiting to enqueue the sentinel and only clearing thread/queue state when stopping the current thread. Adds `flush(timeout)` to wait for queued frames to be fully dispatched, improving control around recording stop/teardown behavior.
1 parent 42bb7a3 commit 3b5ba55

1 file changed

Lines changed: 51 additions & 26 deletions

File tree

‎dlclivegui/services/recording_manager.py‎

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -106,40 +106,38 @@ def pop(self, cam_id: str, default=None) -> VideoRecorder | None:
106106
return self._recorders.pop(cam_id, default)
107107

108108
def _start_dispatcher(self) -> None:
109-
with self._lock:
110-
if self._dispatch_thread is not None and self._dispatch_thread.is_alive():
111-
return
109+
if self._dispatch_thread is not None and self._dispatch_thread.is_alive():
110+
return
112111

113-
self._dispatch_stop.clear()
114-
self._frame_queue = queue.Queue(maxsize=4096)
115-
self._dispatch_thread = threading.Thread(
116-
target=self._dispatch_loop,
117-
name="RecordingManagerDispatcher",
118-
daemon=True,
119-
)
120-
self._dispatch_thread.start()
112+
self._dispatch_stop.clear()
113+
self._frame_queue = queue.Queue(maxsize=4096)
114+
self._dispatch_thread = threading.Thread(
115+
target=self._dispatch_loop,
116+
name="RecordingManagerDispatcher",
117+
daemon=True,
118+
)
119+
self._dispatch_thread.start()
121120

122121
def _stop_dispatcher(self, timeout: float = 2.0) -> None:
123-
self._dispatch_stop.set()
124-
125122
with self._lock:
126123
q = self._frame_queue
127124
t = self._dispatch_thread
128125

129126
if q is not None:
130127
try:
131-
q.put_nowait(_FRAME_SENTINEL)
128+
q.put(_FRAME_SENTINEL, block=True, timeout=timeout)
132129
except queue.Full:
133-
pass
130+
log.warning("Recording frame queue full while stopping dispatcher; dispatcher may not stop promptly.")
134131

135132
if t is not None:
136133
t.join(timeout=timeout)
137134
if t.is_alive():
138135
log.warning("Recording frame dispatcher did not stop within %.1fs", timeout)
139136

140137
with self._lock:
141-
self._dispatch_thread = None
142-
self._frame_queue = None
138+
if self._dispatch_thread is t:
139+
self._dispatch_thread = None
140+
self._frame_queue = None
143141
self._dispatch_stop.clear()
144142

145143
def _dispatch_loop(self) -> None:
@@ -149,11 +147,8 @@ def _dispatch_loop(self) -> None:
149147
if q is None:
150148
return
151149

152-
while not self._dispatch_stop.is_set():
153-
try:
154-
item = q.get(timeout=0.1)
155-
except queue.Empty:
156-
continue
150+
while True:
151+
item = q.get()
157152

158153
try:
159154
if item is _FRAME_SENTINEL:
@@ -270,7 +265,6 @@ def start_all(
270265
self._run_dir = None
271266
return None
272267

273-
self._start_dispatcher()
274268
return run_dir
275269

276270
def stop_all(self) -> None:
@@ -326,13 +320,23 @@ def _write_frame_now(
326320
log.exception("Failed to stop recorder for %s after write error.", cam_id)
327321

328322
def write_frame(
329-
self, cam_id: str, frame: np.ndarray, timestamp: float | None = None, timestamp_metadata: object | None = None
323+
self,
324+
cam_id: str,
325+
frame: np.ndarray,
326+
timestamp: float | None = None,
327+
timestamp_metadata: object | None = None,
330328
) -> None:
331329
with self._lock:
332-
q = self._frame_queue
333330
active = cam_id in self._recorders
331+
if not active:
332+
return
333+
334+
if self._frame_queue is None or self._dispatch_thread is None or not self._dispatch_thread.is_alive():
335+
self._start_dispatcher()
334336

335-
if not active or q is None:
337+
q = self._frame_queue
338+
339+
if q is None:
336340
return
337341

338342
try:
@@ -345,6 +349,27 @@ def write_frame(
345349
getattr(frame, "dtype", None),
346350
)
347351

352+
def flush(self, timeout: float = 2.0) -> bool:
353+
"""Wait until all currently queued recording frames have been dispatched.
354+
355+
Returns True if the queue drained before timeout, False otherwise.
356+
"""
357+
with self._lock:
358+
q = self._frame_queue
359+
360+
if q is None:
361+
return True
362+
363+
done = threading.Event()
364+
365+
def waiter() -> None:
366+
q.join()
367+
done.set()
368+
369+
t = threading.Thread(target=waiter, name="RecordingManagerFlush", daemon=True)
370+
t.start()
371+
return done.wait(timeout)
372+
348373
def get_stats_summary(self) -> str:
349374
totals = {
350375
"enqueued": 0,

0 commit comments

Comments
 (0)