Skip to content

fix(voice): don't leak a play task started while closing - #6788

Open
hassannaftabb wants to merge 1 commit into
livekit:mainfrom
hassannaftabb:fix/background-audio-close-race
Open

fix(voice): don't leak a play task started while closing#6788
hassannaftabb wants to merge 1 commit into
livekit:mainfrom
hassannaftabb:fix/background-audio-close-race

Conversation

@hassannaftabb

Copy link
Copy Markdown

Problem

BackgroundAudioPlayer.aclose() can return while a play task it never cancelled is still running.

aclose() passes *self._play_tasks to cancel_and_wait, so the list is snapshotted at that call. It then awaits twice before clearing self._mixer_atask, which is the only attribute play() gates on. A play() landing in that window is accepted and appends to _play_tasks after the snapshot, so nothing cancels it. The mixer and audio source are then closed underneath it: _run_mixer_task is 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_changed calls play() for the thinking sound, and aclose() unregisters that listener only after the same awaits.

Change

Mark the player closed before the first await in aclose(), and reject play() once closed. The flag is set with no await in between, so nothing can be appended after the snapshot. _agent_state_changed gets the same guard, so a state change arriving before the listener is unregistered cannot start a sound.

Tests

BackgroundAudioPlayer had no tests; these are the first two. Both fail before the change:

  • test_aclose_does_not_leave_a_play_task_running fails on a play task still alive after aclose() returned
  • test_play_handle_from_a_closing_player_never_hangs fails with TimeoutError, the handle never reaching playout-done

Before 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 everything play() and aclose() 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_task is the only caller of AudioSource.capture_frame and 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 --unit gives 1520 passed with the failure set unchanged from main. ruff check, ruff format --check, and mypy --platform linux -p livekit.agents are clean.

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.
@hassannaftabb
hassannaftabb requested a review from a team as a code owner August 11, 2026 11:24

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant