fix(voice): don't leak a play task started while closing - #6788
Open
hassannaftabb wants to merge 1 commit into
Open
fix(voice): don't leak a play task started while closing#6788hassannaftabb wants to merge 1 commit into
hassannaftabb wants to merge 1 commit into
Conversation
BackgroundAudioPlayer.aclose() passes *self._play_tasks to cancel_and_wait, which snapshots the list, and only clears _mixer_atask -- the attribute play() gates on -- after awaiting. A play() landing in that window appends a task nobody cancels, so it outlives a player whose mixer and audio source are already closed. Its PlayHandle never reaches playout-done, so awaiting the handle blocks forever and the task survives teardown. Mark the player closed before the first await and reject play() once closed, so no play task can be appended after the snapshot. _agent_state_changed gets the same guard: aclose() unregisters that listener too, but only after awaiting, so a state change arriving in between could still start a thinking sound. Adds the first unit tests for BackgroundAudioPlayer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
BackgroundAudioPlayer.aclose()can return while a play task it never cancelled is still running.aclose()passes*self._play_taskstocancel_and_wait, so the list is snapshotted at that call. It then awaits twice before clearingself._mixer_atask, which is the only attributeplay()gates on. Aplay()landing in that window is accepted and appends to_play_tasksafter the snapshot, so nothing cancels it. The mixer and audio source are then closed underneath it:_run_mixer_taskis gone, nothing drives the generator,_mark_playout_done()is never called. Awaiting that handle blocks forever and the task outlives the player.This is reachable from the framework itself, not only from direct
play()calls._agent_state_changedcallsplay()for the thinking sound, andaclose()unregisters that listener only after the same awaits.Change
Mark the player closed before the first await in
aclose(), and rejectplay()once closed. The flag is set with no await in between, so nothing can be appended after the snapshot._agent_state_changedgets the same guard, so a state change arriving before the listener is unregistered cannot start a sound.Tests
BackgroundAudioPlayerhad no tests; these are the first two. Both fail before the change:test_aclose_does_not_leave_a_play_task_runningfails on a play task still alive afteraclose()returnedtest_play_handle_from_a_closing_player_never_hangsfails withTimeoutError, the handle never reaching playout-doneBefore the fix they also trip the suite's leaked-task teardown check, which is the same leak seen from the other side. The tests stand the player up without a live room:
start()only adds track publishing, and everythingplay()andaclose()touch is built in__init__.Relationship to #6149 and #6056
I found this while reading the teardown path for those two SIGABRT reports. It does not explain them. The leaked task adds a stream to the mixer, but
_run_mixer_taskis the only caller ofAudioSource.capture_frameand is already cancelled by that point, so nothing here reaches native memory. Those reports still look like a use-after-free below the Python layer. Treating this as a separate, self-contained teardown bug.Verification
pytest --unitgives 1520 passed with the failure set unchanged frommain.ruff check,ruff format --check, andmypy --platform linux -p livekit.agentsare clean.