Running a 271-case end-to-end suite for a skill that speaks and schedules events, the pytest job hangs mid-suite. It reproduces only on a slow runner (GitHub Actions), never on a workstation where the same suite finishes in seconds. Two distinct pile-ups, both in ovoscope.
1. Thread per delivered message (≤ 1.5.0). patched_emit delivered each bus message on its own thread. Across the suite hundreds accumulated, all contending pyee's emitter lock, and the main thread stalled in remove_listener behind them. One job sat at "53%" for 79 minutes before it was cancelled. Upgrading to 1.8.1a1 — which spawns none — removed this, so this half looks already fixed; recording it for anyone still on 1.5.
2. _mock_tts emits while holding self._stop_lock (still in 1.8.1a1). In __init__.py:
_mock_tts takes _stop_lock and calls bus.emit(... "recognizer_loop:audio_output_start") inside it.
- the
_unduck timer takes the same lock and emits recognizer_loop:audio_output_end inside it.
threading.Lock is not reentrant, and these emits are synchronous: whatever a handler does downstream happens with the lock held. If any handler speaks — which a skill legitimately does when an event fires or a round advances — the resulting _mock_tts needs the same lock, and every pending unduck timer queues behind it. The thread dump shows exactly that shape: one thread inside emit at the bus.emit line, several parked at the with self._stop_lock line above it.
~~~ Stack of Thread-32 ~~~
.../ovoscope/__init__.py", line 678, in _unduck # with self._stop_lock
~~~ Stack of Thread-27 ~~~
.../ovoscope/__init__.py", line 681, in _unduck # bus.emit(...)
.../ovoscope/__init__.py", line 283, in patched_emit
The lock is there so stop() and the emit cannot interleave, which is worth keeping. Two options that would keep that property without holding it across a delivery: take a snapshot of _stopped under the lock and emit outside it, or make _stop_lock an RLock so a nested speak on the same thread proceeds.
Environment: ovoscope 1.5.0 and 1.8.1a1, ovos-core 3.5.1a1, ovos-workshop 9.7.4a1, Python 3.13 and 3.14 on ubuntu-latest.
Worked around downstream by keeping the skill's scheduled event from firing during the suite, which removes our trigger but not the re-entrancy.
Running a 271-case end-to-end suite for a skill that speaks and schedules events, the pytest job hangs mid-suite. It reproduces only on a slow runner (GitHub Actions), never on a workstation where the same suite finishes in seconds. Two distinct pile-ups, both in ovoscope.
1. Thread per delivered message (≤ 1.5.0).
patched_emitdelivered each bus message on its own thread. Across the suite hundreds accumulated, all contending pyee's emitter lock, and the main thread stalled inremove_listenerbehind them. One job sat at "53%" for 79 minutes before it was cancelled. Upgrading to 1.8.1a1 — which spawns none — removed this, so this half looks already fixed; recording it for anyone still on 1.5.2.
_mock_ttsemits while holdingself._stop_lock(still in 1.8.1a1). In__init__.py:_mock_ttstakes_stop_lockand callsbus.emit(... "recognizer_loop:audio_output_start")inside it._unducktimer takes the same lock and emitsrecognizer_loop:audio_output_endinside it.threading.Lockis not reentrant, and these emits are synchronous: whatever a handler does downstream happens with the lock held. If any handler speaks — which a skill legitimately does when an event fires or a round advances — the resulting_mock_ttsneeds the same lock, and every pending unduck timer queues behind it. The thread dump shows exactly that shape: one thread insideemitat thebus.emitline, several parked at thewith self._stop_lockline above it.The lock is there so
stop()and the emit cannot interleave, which is worth keeping. Two options that would keep that property without holding it across a delivery: take a snapshot of_stoppedunder the lock and emit outside it, or make_stop_lockanRLockso a nested speak on the same thread proceeds.Environment: ovoscope 1.5.0 and 1.8.1a1, ovos-core 3.5.1a1, ovos-workshop 9.7.4a1, Python 3.13 and 3.14 on ubuntu-latest.
Worked around downstream by keeping the skill's scheduled event from firing during the suite, which removes our trigger but not the re-entrancy.