Skip to content

Commit 60da106

Browse files
committed
Refactor VideoRecorder.stop lifecycle handling
Detect and early-return when recorder is already stopped or recovered from an abandoned state, and reduce the time holding the lifecycle lock by moving queue/thread/writer shutdown work outside the lock. Ensure sentinel is enqueued, writer thread is joined with timeout (marking recorder as abandoned and recording an encode_error if it remains alive), close the writer cleanly, save timestamps, and finally clear lifecycle fields under the lock. Improves concurrency, cleanup correctness, and prevents restarting a recorder that was marked abandoned.
1 parent 71bb1aa commit 60da106

File tree

1 file changed

+31
-26
lines changed

1 file changed

+31
-26
lines changed

dlclivegui/services/video_recorder.py

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ def write(self, frame: np.ndarray, timestamp: float | None = None) -> bool:
211211

212212
def stop(self) -> None:
213213
with self._lifecycle_lock:
214-
if self._writer is None and not self.is_running:
214+
already_stopped = (self._writer is None) and (not self.is_running)
215+
if already_stopped:
215216
# If the recorder was previously marked as abandoned because the
216217
# writer thread did not stop in time, but the thread has since
217218
# exited, perform cleanup so the recorder can become fully stopped
@@ -222,39 +223,43 @@ def stop(self) -> None:
222223
self._queue = None
223224
self._stop_event.clear()
224225
self._abandoned = False
226+
return
225227

226228
self._stop_event.set()
227-
228229
q = self._queue
229-
if q is not None:
230-
try:
231-
q.put_nowait(_SENTINEL)
232-
except queue.Full:
233-
pass
234-
235230
t = self._writer_thread
236-
if t is not None:
237-
t.join(timeout=STOP_JOIN_TIMEOUT)
238-
if t.is_alive():
239-
with self._stats_lock:
240-
self._encode_error = RuntimeError(
241-
"Failed to stop VideoRecorder within timeout; thread is still alive."
242-
)
231+
writer = self._writer
232+
233+
if q is not None:
234+
try:
235+
q.put_nowait(_SENTINEL)
236+
except queue.Full:
237+
pass
238+
239+
if t is not None:
240+
t.join(timeout=STOP_JOIN_TIMEOUT)
241+
if t.is_alive():
242+
with self._stats_lock:
243+
self._encode_error = RuntimeError(
244+
"Failed to stop VideoRecorder within timeout; thread is still alive."
245+
)
246+
with self._lifecycle_lock:
243247
self._abandoned = True
244-
logger.critical(
245-
"Failed to stop VideoRecorder within timeout; thread is still alive. "
246-
"Marking recorder as abandoned to prevent restart."
247-
)
248-
return
248+
logger.critical(
249+
"Failed to stop VideoRecorder within timeout; thread is still alive. "
250+
"Marking recorder as abandoned to prevent restart."
251+
)
252+
return
249253

250-
if self._writer is not None:
251-
try:
252-
self._writer.close()
253-
except Exception:
254-
logger.exception("Failed to close WriteGear cleanly")
254+
if writer is not None:
255+
try:
256+
writer.close()
257+
except Exception:
258+
logger.exception("Failed to close WriteGear cleanly")
255259

256-
self._save_timestamps()
260+
self._save_timestamps()
257261

262+
with self._lifecycle_lock:
258263
self._writer = None
259264
self._writer_thread = None
260265
self._queue = None

0 commit comments

Comments
 (0)