Skip to content

Commit bd85dce

Browse files
committed
Support customizing reconnection delays.
Fix #1395 (again).
1 parent 7f71b99 commit bd85dce

3 files changed

Lines changed: 27 additions & 16 deletions

File tree

docs/project/changelog.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,17 @@ New features
5959
* Added the ``--insecure`` option to the ``websockets`` CLI to disable TLS
6060
certificate validation.
6161

62+
Improvements
63+
............
64+
65+
* Added the ``reconnect_delays`` argument for customizing the delays between
66+
reconnection attempts in :func:`~asyncio.client.connect`, beyond existing
67+
``WEBSOCKETS_BACKOFF_*`` environment variables.
68+
6269
Bug fixes
6370
.........
6471

65-
* * Restored compatibility of the ``websockets`` CLI with Windows.
72+
* Restored compatibility of the ``websockets`` CLI with Windows.
6673

6774
.. _16.1:
6875

src/websockets/asyncio/client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ class connect:
234234
:obj:`None` disables timeouts.
235235
close_timeout: Timeout for closing the connection in seconds.
236236
:obj:`None` disables the timeout.
237+
reconnect_delays: Delays in seconds between reconnection attempts.
238+
Default is exponential backoff with 5s jitter, capped at 60s.
237239
max_size: Maximum size of incoming messages in bytes.
238240
:obj:`None` disables the limit. You may pass a ``(max_message_size,
239241
max_fragment_size)`` tuple to set different limits for messages and
@@ -314,13 +316,14 @@ def __init__(
314316
ping_interval: float | None = 20,
315317
ping_timeout: float | None = 20,
316318
close_timeout: float | None = 10,
319+
reconnect_delays: Callable[[], Generator[float]] = backoff,
317320
# Limits
318321
max_size: int | None | tuple[int | None, int | None] = 2**20,
319322
max_queue: int | None | tuple[int | None, int | None] = 16,
320323
write_limit: int | tuple[int, int | None] = 2**15,
321324
# Logging
322325
logger: LoggerLike | None = None,
323-
# Escape hatch for advanced customization
326+
# Escape hatches for advanced customization
324327
create_connection: type[ClientConnection] | None = None,
325328
# Other keyword arguments are passed to loop.create_connection
326329
**kwargs: Any,
@@ -368,6 +371,7 @@ def protocol_factory(uri: WebSocketURI) -> ClientConnection:
368371
self.user_agent_header = user_agent_header
369372
self.process_exception = process_exception
370373
self.open_timeout = open_timeout
374+
self.reconnect_delays = reconnect_delays
371375
self.logger = logger
372376
self.connection_kwargs = kwargs
373377

@@ -630,7 +634,7 @@ async def __aiter__(self) -> AsyncIterator[ClientConnection]:
630634
# The connection failed with a retryable error.
631635
# Start or continue backoff and reconnect.
632636
if delays is None:
633-
delays = backoff()
637+
delays = self.reconnect_delays()
634638
delay = next(delays)
635639
self.logger.info(
636640
"connect failed; reconnecting in %.1f seconds: %s",

tests/asyncio/test_client.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,14 @@
2929
from .server import args, get_host_port, get_uri, handler
3030

3131

32-
@contextlib.asynccontextmanager
33-
async def short_backoff_delay():
32+
def short_backoff():
3433
defaults = backoff.__defaults__
35-
backoff.__defaults__ = (
34+
yield from backoff(
3635
defaults[0] * MS,
3736
defaults[1] * MS,
3837
defaults[2] * MS,
3938
defaults[3],
4039
)
41-
try:
42-
yield
43-
finally:
44-
backoff.__defaults__ = defaults
4540

4641

4742
@contextlib.asynccontextmanager
@@ -157,7 +152,6 @@ def create_connection(*args, **kwargs):
157152
) as client:
158153
self.assertTrue(client.create_connection_ran)
159154

160-
@short_backoff_delay()
161155
async def test_reconnect(self):
162156
"""Client reconnects to server."""
163157
iterations = 0
@@ -179,7 +173,11 @@ async def process_request(connection, request):
179173

180174
async with serve(*args, process_request=process_request) as server:
181175
with self.assertRaises(InvalidStatus) as raised:
182-
async for client in connect(get_uri(server), open_timeout=3 * MS):
176+
async for client in connect(
177+
get_uri(server),
178+
open_timeout=3 * MS,
179+
reconnect_delays=short_backoff,
180+
):
183181
self.assertEqual(client.protocol.state.name, "OPEN")
184182
successful += 1
185183

@@ -190,7 +188,6 @@ async def process_request(connection, request):
190188
self.assertEqual(iterations, 6)
191189
self.assertEqual(successful, 2)
192190

193-
@short_backoff_delay()
194191
async def test_reconnect_with_custom_process_exception(self):
195192
"""Client runs process_exception to tell if errors are retryable or fatal."""
196193
iteration = 0
@@ -213,7 +210,9 @@ def process_exception(exc):
213210
async with serve(*args, process_request=process_request) as server:
214211
with self.assertRaises(Exception) as raised:
215212
async for _ in connect(
216-
get_uri(server), process_exception=process_exception
213+
get_uri(server),
214+
process_exception=process_exception,
215+
reconnect_delays=short_backoff,
217216
):
218217
self.fail("did not raise")
219218

@@ -223,7 +222,6 @@ def process_exception(exc):
223222
"🫖 💔 ☕️",
224223
)
225224

226-
@short_backoff_delay()
227225
async def test_reconnect_with_custom_process_exception_raising_exception(self):
228226
"""Client supports raising an exception in process_exception."""
229227

@@ -238,7 +236,9 @@ def process_exception(exc):
238236
async with serve(*args, process_request=process_request) as server:
239237
with self.assertRaises(Exception) as raised:
240238
async for _ in connect(
241-
get_uri(server), process_exception=process_exception
239+
get_uri(server),
240+
process_exception=process_exception,
241+
reconnect_delays=short_backoff,
242242
):
243243
self.fail("did not raise")
244244

0 commit comments

Comments
 (0)