Skip to content

fix(aio): send one liveness probe per silence window - #8257

Open
BlueX888 wants to merge 1 commit into
livekit:mainfrom
BlueX888:fix/ws-monitor-single-liveness-probe
Open

BlueX888 wants to merge 1 commit into
livekit:mainfrom
BlueX888:fix/ws-monitor-single-liveness-probe

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

Problem

WebSocketMonitor.receive() sends a PING whenever asyncio.wait() returns without a completed task, even when the liveness deadline has not been reached:

if self._loop.time() - self._last_received >= self._liveness_timeout:
    raise aiohttp.ServerTimeoutError(...)
self._ping_sent = True
ping = asyncio.create_task(self._ws.ping())

At that point the wait's remaining budget is deadline - now. When that budget is smaller than the loop's timer resolution (observed on Windows, ~15.6 ms), asyncio.wait() can return immediately, so the loop probes again, wakes again, and repeats until the clock finally crosses the deadline. One unanswered probe produced 113 PINGs at the same timestamp before the liveness error:

0.187 client sending ping #113
0.187 server got PING #113
0.203 APIConnectionError: WebSocket connection lost (caused by ServerTimeoutError: No WebSocket messages received for 0.2 seconds)

The existing test tests/test_websocket_monitor.py::test_outbound_audio_does_not_hide_unanswered_ping catches this on Windows (assert 113 == 1 at assert pings == 1); it passes on Linux, where the timer is precise enough that the burst does not happen.

Fix

Once the first probe is out, an empty wake can only be an early return. Keep waiting for the liveness deadline instead of probing again; a PONG (or any incoming frame) still resets the window through the receiver path, and the timeout still fires at liveness_timeout.

Tests

  • New test_early_wake_does_not_resend_the_liveness_probe simulates one early wake deterministically: it fails on main (three PINGs) and passes with the change, so the behavior is covered on every platform rather than only where the timer rounds.
  • tests/test_websocket_monitor.py: 13 passed.
  • tests/test_inference_stt_connection.py tests/test_inference_stt_fallback.py: 80 passed.
  • ruff check and ruff format --check: clean.

Note

Maintainer PR #8147 also edits websocket.py (close/error handling) but leaves this probe logic untouched, so the two changes are compatible; happy to fold this into that PR instead if you prefer to batch.

🤖 Generated with Claude Code

`WebSocketMonitor.receive()` sends a PING whenever `asyncio.wait()` returns
without a completed task, even when the liveness deadline has not passed. On
Windows the event loop's timer granularity makes a wait whose remaining budget
is smaller than one tick return immediately; the loop then re-sends a probe and
continues, so a peer that never answers sees dozens of PINGs at the same
instant instead of one. The upstream test
`tests/test_websocket_monitor.py::test_outbound_audio_does_not_hide_unanswered_ping`
observes 67-113 PINGs there (it asserts 1) while it passes on Linux.

Once the first probe is out, an empty wake can only be an early return. Keep
waiting for the liveness deadline instead of probing again; an answered probe
still resets the window through the receiver path.

Add a deterministic regression test that makes one wait wake early. It fails on
main (3 pings) and passes with the change, so the behavior is covered on every
platform rather than only where the timer rounds.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@BlueX888
BlueX888 requested a review from a team as a code owner October 29, 2026 16:11

@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 bugs or issues to report.

Devin Review

This branch has not been deployed

No deployments
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