Skip to content

feat(server): single-session lifetime and relay-only ICE for serverle… - #107

Open
berges99 wants to merge 5 commits into
mainfrom
feat/voice-serverless-rtc
Open

feat(server): single-session lifetime and relay-only ICE for serverle…#107
berges99 wants to merge 5 commits into
mainfrom
feat/voice-serverless-rtc

Conversation

@berges99

Copy link
Copy Markdown
Collaborator

…ss voice boxes

TIMBAL_VOICE_SINGLE_SESSION=1 makes the server serve exactly one voice session (WS or RTC) and exit 0 — after draining the platform recording upload, since the process is the only thing holding that data. Boxes nobody connects to exit after TIMBAL_VOICE_IDLE_EXIT_SECS (default 60, boot → media established); a second session gets 409 (RTC) / 1008 (WS). Needed on serverless workforce deployments where the platform can't see the call (media flows browser ↔ TURN ↔ box) and reaps on process exit.

TIMBAL_VOICE_RTC_FORCE_RELAY=1 restricts ICE to the TURN relay: skip STUN, allocate on TIMBAL_TURN_URL, and strip host/srflx candidates from the answer SDP (aiortc has no iceTransportPolicy) — private-subnet boxes advertise only reachable candidates. Degrades loudly to all candidates when TURN is unconfigured or the allocation yields no relay candidate.

…ss voice boxes

TIMBAL_VOICE_SINGLE_SESSION=1 makes the server serve exactly one voice
session (WS or RTC) and exit 0 — after draining the platform recording
upload, since the process is the only thing holding that data. Boxes
nobody connects to exit after TIMBAL_VOICE_IDLE_EXIT_SECS (default 60,
boot → media established); a second session gets 409 (RTC) / 1008 (WS).
Needed on serverless workforce deployments where the platform can't see
the call (media flows browser ↔ TURN ↔ box) and reaps on process exit.

TIMBAL_VOICE_RTC_FORCE_RELAY=1 restricts ICE to the TURN relay: skip
STUN, allocate on TIMBAL_TURN_URL, and strip host/srflx candidates from
the answer SDP (aiortc has no iceTransportPolicy) — private-subnet boxes
advertise only reachable candidates. Degrades loudly to all candidates
when TURN is unconfigured or the allocation yields no relay candidate.
@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes core voice server lifecycle (process exit, slot claiming) and WebRTC signaling/SDP; mistakes could strand boxes, drop recordings on exit, or break connectivity when force-relay is misconfigured.

Overview
Targets serverless voice boxes where the platform spawns one process per call and reaps on exit (WebRTC media is invisible to the platform).

Single-session lifetime (TIMBAL_VOICE_SINGLE_SESSION=1): a new SingleSessionGuard is armed at HTTP lifespan start (env read post-CRIU, not at import). The first WebSocket or WebRTC session claims the process; when it ends the server drains in-flight recording uploads via drain_upload_tasks() then os._exit(0). If no media connects within TIMBAL_VOICE_IDLE_EXIT_SECS (default 60), it exits the same way. A second session gets 409 on POST /voice/rtc or WebSocket close 1008; rejected offers (400) release the slot; setup failures after claim also release so the idle timer can bound lifetime. WebRTC marks connected on connectionState === "connected"; WebSocket treats accept as connected.

Relay-only ICE (TIMBAL_VOICE_RTC_FORCE_RELAY=1 + TIMBAL_TURN_URL): skips STUN, uses TURN-only ICE servers, and strips host/srflx candidates from the answer SDP (aiortc has no iceTransportPolicy). Degrades to all candidates if TURN is missing or allocation yields no relay.

Supporting fix: Smart Turn and Namo ONNX loaders switch from asyncio.Lock to threading.Lock so lifespan warmup and request handlers on different event loops (e.g. TestClient) do not break the first voice session.

Extensive tests and README updates document the new env vars and HTTP status codes.

Reviewed by Cursor Bugbot for commit 333f453. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread python/timbal/server/voice.py
Comment thread python/timbal/server/single_session.py
Comment thread python/timbal/server/rtc.py Outdated
… RTC ICE

Guarantee finish() after WS claim so handshake/session-build failures still
exit; route idle exit through finish() so in-flight recording uploads drain;
stop treating WebRTC disconnected as terminal (transient ICE blips).
Comment thread python/timbal/server/rtc.py
A claim() that fails before the driver task starts (e.g. build_voice_session)
left the slot claimed with no finish()/release, so retries got 409 until idle
exit. Mirror the 400 paths: release on setup exceptions and re-raise the 500.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9a14b46. Configure here.

Comment thread python/timbal/server/single_session.py Outdated
berges99 added 2 commits July 29, 2026 22:54
_idle_exit cleared the timer task and always finish()'d without checking
_connected, so a WebRTC connect at the idle deadline could still exit 0.
Yield once for queued ICE handlers, re-check through the upload drain, and
only then commit the exit.
asyncio.Lock on the process-wide EOU singletons breaks under Starlette
TestClient on Windows/py3.13: lifespan warmup binds the lock (often while
stuck on HF 429), then the WS handler runs on another loop. Load is already
executor work, so a thread lock is the right primitive.
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