Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions livekit-rtc/livekit/rtc/audio_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,6 @@ def __init__(
self._processor = noise_cancellation
self._processor_auto_close = auto_close_noise_cancellation

self._task = self._loop.create_task(self._run())
self._task.add_done_callback(task_done_logger)

stream: Any = None
if "participant" in kwargs:
stream = self._create_owned_stream_from_participant(
Expand All @@ -138,6 +135,11 @@ def __init__(
self._ffi_handle = FfiHandle(stream.handle.id)
self._info = stream.info

# Start _run only after _ffi_handle is set, so a failure above doesn't orphan
# a task that dereferences an unassigned _ffi_handle.
self._task = self._loop.create_task(self._run())
self._task.add_done_callback(task_done_logger)

if self._track is not None:
self._track._register_audio_stream(self)

Expand Down
53 changes: 53 additions & 0 deletions livekit-rtc/tests/test_audio_stream_init.py

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.

🚩 Test may not be discovered by root pytest configuration

The root pyproject.toml sets testpaths = ["tests"] which points to the repo-root tests/ directory. The new test file lives in livekit-rtc/tests/, which is a separate directory. Unless pytest is invoked from within livekit-rtc/ or with an explicit path, this test may not be collected by the default pytest invocation from the repo root. Other tests in livekit-rtc/tests/ (e.g., test_audio.py) appear to be integration/E2E tests that may be run separately, so this might be intentional — but worth confirming the CI configuration discovers it.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good flag, but CI does collect it. The test jobs run pytest tests/ livekit-rtc/tests/ with both paths explicit (tests.yml), which overrides the testpaths = ["tests"] default. It shows up green in this PR's run, e.g. the ubuntu-3.9 job: livekit-rtc/tests/test_audio_stream_init.py ..

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2026 LiveKit, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Regression tests for AudioStream.__init__ error handling."""

from __future__ import annotations

import asyncio
from unittest.mock import MagicMock, patch

import pytest

from livekit import rtc


async def test_failed_stream_creation_does_not_orphan_run_task() -> None:
"""If owned-stream creation fails, __init__ must not leave a running _run task.

_run dereferences self._ffi_handle, which is only assigned once stream creation
succeeds. Scheduling the task before that assignment leaves an orphaned task that
raises AttributeError from the event loop, uncatchable by the caller.
"""
track = MagicMock(spec=rtc.Track)
before = asyncio.all_tasks()

with patch.object(
rtc.AudioStream,
"_create_owned_stream",
side_effect=RuntimeError("track already closed"),
):
with pytest.raises(RuntimeError, match="track already closed"):
rtc.AudioStream(track)

orphaned = [
t
for t in asyncio.all_tasks() - before
if getattr(t.get_coro(), "__qualname__", "") == "AudioStream._run"
]
for t in orphaned:
t.cancel()

assert not orphaned
Loading