Skip to content

Commit 16ccac5

Browse files
authored
Start AudioStream _run task after the FFI handle is assigned (#742)
1 parent 357e5db commit 16ccac5

2 files changed

Lines changed: 58 additions & 3 deletions

File tree

livekit-rtc/livekit/rtc/audio_stream.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,6 @@ def __init__(
125125
self._processor = noise_cancellation
126126
self._processor_auto_close = auto_close_noise_cancellation
127127

128-
self._task = self._loop.create_task(self._run())
129-
self._task.add_done_callback(task_done_logger)
130-
131128
stream: Any = None
132129
if "participant" in kwargs:
133130
stream = self._create_owned_stream_from_participant(
@@ -138,6 +135,11 @@ def __init__(
138135
self._ffi_handle = FfiHandle(stream.handle.id)
139136
self._info = stream.info
140137

138+
# Start _run only after _ffi_handle is set, so a failure above doesn't orphan
139+
# a task that dereferences an unassigned _ffi_handle.
140+
self._task = self._loop.create_task(self._run())
141+
self._task.add_done_callback(task_done_logger)
142+
141143
if self._track is not None:
142144
self._track._register_audio_stream(self)
143145

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright 2026 LiveKit, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Regression tests for AudioStream.__init__ error handling."""
16+
17+
from __future__ import annotations
18+
19+
import asyncio
20+
from unittest.mock import MagicMock, patch
21+
22+
import pytest
23+
24+
from livekit import rtc
25+
26+
27+
async def test_failed_stream_creation_does_not_orphan_run_task() -> None:
28+
"""If owned-stream creation fails, __init__ must not leave a running _run task.
29+
30+
_run dereferences self._ffi_handle, which is only assigned once stream creation
31+
succeeds. Scheduling the task before that assignment leaves an orphaned task that
32+
raises AttributeError from the event loop, uncatchable by the caller.
33+
"""
34+
track = MagicMock(spec=rtc.Track)
35+
before = asyncio.all_tasks()
36+
37+
with patch.object(
38+
rtc.AudioStream,
39+
"_create_owned_stream",
40+
side_effect=RuntimeError("track already closed"),
41+
):
42+
with pytest.raises(RuntimeError, match="track already closed"):
43+
rtc.AudioStream(track)
44+
45+
orphaned = [
46+
t
47+
for t in asyncio.all_tasks() - before
48+
if getattr(t.get_coro(), "__qualname__", "") == "AudioStream._run"
49+
]
50+
for t in orphaned:
51+
t.cancel()
52+
53+
assert not orphaned

0 commit comments

Comments
 (0)