Skip to content

Commit 2cf9b73

Browse files
committed
refactor: use exp_backoff with sleep parameter in connect() retry loop
Add sleep parameter to exp_backoff so callers don't need to manually call asyncio.sleep. Replaces inline backoff calculation in connect().
1 parent 61da611 commit 2cf9b73

File tree

3 files changed

+57
-7
lines changed

3 files changed

+57
-7
lines changed

getstream/video/rtc/connection_manager.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
join_call,
2626
watch_call,
2727
)
28+
from getstream.video.rtc.coordinator.backoff import exp_backoff
2829
from getstream.video.rtc.track_util import (
2930
fix_sdp_msid_semantic,
3031
fix_sdp_rtcp_fb,
@@ -460,7 +461,21 @@ def _on_coordinator_task_done(task: asyncio.Task):
460461
failed_sfus: list[str] = []
461462
last_error: Optional[SfuJoinError] = None
462463

463-
for attempt in range(1 + self._max_join_retries):
464+
# First attempt without delay
465+
attempt = 0
466+
try:
467+
await self._connect_internal()
468+
return
469+
except SfuJoinError as e:
470+
last_error = e
471+
self._handle_join_failure(e, attempt, failed_sfus)
472+
473+
# Retries with exponential backoff, requesting a different SFU
474+
async for delay in exp_backoff(
475+
max_retries=self._max_join_retries, base=0.5, sleep=True
476+
):
477+
attempt += 1
478+
logger.info(f"Retrying with different SFU (waited {delay}s)...")
464479
try:
465480
await self._connect_internal(
466481
migrating_from_list=failed_sfus if failed_sfus else None,
@@ -470,11 +485,6 @@ def _on_coordinator_task_done(task: asyncio.Task):
470485
last_error = e
471486
self._handle_join_failure(e, attempt, failed_sfus)
472487

473-
if attempt < self._max_join_retries:
474-
delay = 0.5 * (2.0**attempt)
475-
logger.info(f"Retrying in {delay}s with different SFU...")
476-
await asyncio.sleep(delay)
477-
478488
raise last_error # type: ignore[misc]
479489

480490
def _handle_join_failure(

getstream/video/rtc/coordinator/backoff.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
when reconnecting to failed WebSocket connections.
66
"""
77

8+
import asyncio
89
import logging
910
from typing import AsyncIterator
1011

1112
logger = logging.getLogger(__name__)
1213

1314

1415
async def exp_backoff(
15-
max_retries: int, base: float = 1.0, factor: float = 2.0
16+
max_retries: int,
17+
base: float = 1.0,
18+
factor: float = 2.0,
19+
sleep: bool = False,
1620
) -> AsyncIterator[float]:
1721
"""
1822
Generate exponential backoff delays for retry attempts.
@@ -21,6 +25,7 @@ async def exp_backoff(
2125
max_retries: Maximum number of retry attempts
2226
base: Base delay in seconds for the first retry
2327
factor: Multiplicative factor for each subsequent retry
28+
sleep: If True, sleep for the delay before yielding
2429
2530
Yields:
2631
float: Delay in seconds for each retry attempt
@@ -39,4 +44,6 @@ async def exp_backoff(
3944
for attempt in range(max_retries):
4045
delay = base * (factor**attempt)
4146
logger.debug(f"Backoff attempt {attempt + 1}/{max_retries}: {delay}s delay")
47+
if sleep:
48+
await asyncio.sleep(delay)
4249
yield delay

tests/rtc/coordinator/test_backoff.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import pytest
6+
from unittest.mock import patch, AsyncMock
67
from getstream.video.rtc.coordinator.backoff import exp_backoff
78

89

@@ -84,3 +85,35 @@ async def test_exp_backoff_fractional_factor():
8485
assert actual_delays == expected_delays, (
8586
f"Expected delays {expected_delays}, but got {actual_delays}"
8687
)
88+
89+
90+
@pytest.mark.asyncio
91+
async def test_exp_backoff_sleep():
92+
"""Test that sleep=True calls asyncio.sleep with each delay."""
93+
expected_delays = [0.5, 1.0, 2.0]
94+
95+
with patch(
96+
"getstream.video.rtc.coordinator.backoff.asyncio.sleep",
97+
new_callable=AsyncMock,
98+
) as mock_sleep:
99+
actual_delays = []
100+
async for delay in exp_backoff(max_retries=3, base=0.5, sleep=True):
101+
actual_delays.append(delay)
102+
103+
assert actual_delays == expected_delays
104+
assert mock_sleep.await_count == 3
105+
for expected, call in zip(expected_delays, mock_sleep.await_args_list):
106+
assert call.args[0] == expected
107+
108+
109+
@pytest.mark.asyncio
110+
async def test_exp_backoff_no_sleep_by_default():
111+
"""Test that sleep=False (default) does not call asyncio.sleep."""
112+
with patch(
113+
"getstream.video.rtc.coordinator.backoff.asyncio.sleep",
114+
new_callable=AsyncMock,
115+
) as mock_sleep:
116+
async for _ in exp_backoff(max_retries=3):
117+
pass
118+
119+
mock_sleep.assert_not_awaited()

0 commit comments

Comments
 (0)