Skip to content

Commit 6761850

Browse files
committed
improve timeout behavior
standard API times out in 10s, and SIP dial APIs defaults to 30 we were not using a timeout correctly, which could affect retry behavior
1 parent 2a38ec0 commit 6761850

4 files changed

Lines changed: 40 additions & 19 deletions

File tree

livekit-api/livekit/api/_failover.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,28 @@
3131

3232
FAILOVER_MAX_ATTEMPTS = 3
3333
FAILOVER_BACKOFF_BASE = 0.2 # seconds
34-
35-
36-
def failover_attempts(enabled: bool, host: Optional[str], force: bool = False) -> int:
34+
# Below this per-request timeout (seconds) a retry is unlikely to help and many
35+
# clients would retry in lockstep across regions, so a short request gets a
36+
# single attempt (thundering-herd guard).
37+
MIN_FAILOVER_TIMEOUT = 5.0
38+
39+
40+
def failover_attempts(
41+
enabled: bool,
42+
host: Optional[str],
43+
force: bool = False,
44+
timeout: Optional[float] = None,
45+
) -> int:
3746
"""Total request attempts for a host; 1 means no failover. Failover only
38-
engages when enabled and the host is a LiveKit Cloud domain. ``force``
39-
bypasses the cloud-host check and is for internal testing only.
47+
engages when enabled, the host is a LiveKit Cloud domain, and the request
48+
timeout is long enough to retry. ``force`` bypasses the cloud-host check and
49+
is for internal testing only.
4050
"""
41-
if enabled and (force or (host is not None and is_cloud(host))):
42-
return FAILOVER_MAX_ATTEMPTS
43-
return 1
51+
if not (enabled and (force or (host is not None and is_cloud(host)))):
52+
return 1
53+
if timeout is not None and 0 < timeout < MIN_FAILOVER_TIMEOUT:
54+
return 1
55+
return FAILOVER_MAX_ATTEMPTS
4456

4557

4658
def is_cloud(host: str) -> bool:

livekit-api/livekit/api/livekit_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(
3939
url: LiveKit server URL (read from `LIVEKIT_URL` environment variable if not provided)
4040
api_key: API key (read from `LIVEKIT_API_KEY` environment variable if not provided)
4141
api_secret: API secret (read from `LIVEKIT_API_SECRET` environment variable if not provided)
42-
timeout: Request timeout (default: 60 seconds)
42+
timeout: Request timeout (default: 10 seconds)
4343
session: aiohttp.ClientSession instance to use for requests, if not provided, a new one will be created
4444
"""
4545
url = url or os.getenv("LIVEKIT_URL")
@@ -57,7 +57,7 @@ def __init__(
5757
if not self._session:
5858
self._custom_session = False
5959
if not timeout:
60-
timeout = aiohttp.ClientTimeout(total=60)
60+
timeout = aiohttp.ClientTimeout(total=10)
6161
self._session = aiohttp.ClientSession(timeout=timeout)
6262

6363
self._room = RoomService(self._session, url, api_key, api_secret, failover)

livekit-api/livekit/api/sip_service.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
from .access_token import VideoGrants, SIPGrants
3939

4040
SVC = "SIP"
41+
42+
# Calls that dial a phone (CreateSIPParticipant with wait_until_answered,
43+
# TransferSIPParticipant) take longer than a normal request.
44+
SIP_DIAL_TIMEOUT = 30.0
4145
"""@private"""
4246

4347

@@ -782,16 +786,12 @@ async def create_sip_participant(
782786
"""
783787
client_timeout: Optional[aiohttp.ClientTimeout] = None
784788
if timeout:
785-
# obay user specified timeout
789+
# obey user specified timeout
786790
client_timeout = aiohttp.ClientTimeout(total=timeout)
787791
elif create.wait_until_answered:
788-
# ensure default timeout isn't too short when using sync mode
789-
if (
790-
self._client._session.timeout
791-
and self._client._session.timeout.total
792-
and self._client._session.timeout.total < 20
793-
):
794-
client_timeout = aiohttp.ClientTimeout(total=20)
792+
# Dialing a phone and waiting for an answer takes longer than a
793+
# normal call, so use a longer default.
794+
client_timeout = aiohttp.ClientTimeout(total=SIP_DIAL_TIMEOUT)
795795

796796
if trunk_id:
797797
create.sip_trunk_id = trunk_id
@@ -831,6 +831,8 @@ async def transfer_sip_participant(
831831
sip=SIPGrants(call=True),
832832
),
833833
SIPParticipantInfo,
834+
# Transferring a call dials a phone, which takes longer than normal.
835+
timeout=aiohttp.ClientTimeout(total=SIP_DIAL_TIMEOUT),
834836
)
835837

836838
def _admin_headers(self) -> dict[str, str]:

livekit-api/livekit/api/twirp_client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,15 @@ async def request(
150150
headers["Content-Type"] = "application/protobuf"
151151
serialized_data = data.SerializeToString()
152152

153+
# The effective per-attempt timeout is the per-call override, or the
154+
# session default; used to gate failover for short requests.
155+
effective_timeout = timeout.total if timeout else None
156+
if effective_timeout is None and self._session.timeout is not None:
157+
effective_timeout = self._session.timeout.total
153158
host = urlparse(self._origin).hostname
154-
max_attempts = failover_attempts(self._failover, host, self._failover_force)
159+
max_attempts = failover_attempts(
160+
self._failover, host, self._failover_force, effective_timeout
161+
)
155162
attempted = {host_key(self._origin)}
156163
region_origins: Optional[List[str]] = None
157164
current_origin = self._origin

0 commit comments

Comments
 (0)