Skip to content

Commit 033bf40

Browse files
committed
Add a generator of backoff delays.
1 parent 04ac475 commit 033bf40

File tree

4 files changed

+86
-9
lines changed

4 files changed

+86
-9
lines changed

docs/reference/variables.rst

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Logging
1313
See the :doc:`logging guide <../topics/logging>` for details.
1414

1515
Security
16-
........
16+
--------
1717

1818
.. envvar:: WEBSOCKETS_SERVER
1919

@@ -31,18 +31,49 @@ Security
3131

3232
Maximum length of the request or status line in the opening handshake.
3333

34-
The default value is ``8192``.
34+
The default value is ``8192`` bytes.
3535

3636
.. envvar:: WEBSOCKETS_MAX_NUM_HEADERS
3737

3838
Maximum number of HTTP headers in the opening handshake.
3939

40-
The default value is ``128``.
40+
The default value is ``128`` bytes.
4141

4242
.. envvar:: WEBSOCKETS_MAX_BODY_SIZE
4343

4444
Maximum size of the body of an HTTP response in the opening handshake.
4545

46-
The default value is ``1_048_576`` (1 MiB).
46+
The default value is ``1_048_576`` bytes (1 MiB).
4747

4848
See the :doc:`security guide <../topics/security>` for details.
49+
50+
Reconnection
51+
------------
52+
53+
Reconnection attempts are spaced out with truncated exponential backoff.
54+
55+
.. envvar:: BACKOFF_INITIAL_DELAY
56+
57+
The first attempt is delayed by a random amount of time between ``0`` and
58+
``BACKOFF_INITIAL_DELAY`` seconds.
59+
60+
The default value is ``5.0`` seconds.
61+
62+
.. envvar:: BACKOFF_MIN_DELAY
63+
64+
The second attempt is delayed by ``BACKOFF_MIN_DELAY`` seconds.
65+
66+
The default value is ``3.1`` seconds.
67+
68+
.. envvar:: BACKOFF_FACTOR
69+
70+
After the second attempt, the delay is multiplied by ``BACKOFF_FACTOR``
71+
between each attempt.
72+
73+
The default value is ``1.618``.
74+
75+
.. envvar:: BACKOFF_MAX_DELAY
76+
77+
The delay between attempts is capped at ``BACKOFF_MAX_DELAY`` seconds.
78+
79+
The default value is ``90.0`` seconds.

src/websockets/client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import os
4+
import random
35
import warnings
46
from typing import Any, Generator, Sequence
57

@@ -357,3 +359,33 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
357359
DeprecationWarning,
358360
)
359361
super().__init__(*args, **kwargs)
362+
363+
364+
BACKOFF_INITIAL_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_INITIAL_DELAY", "5"))
365+
BACKOFF_MIN_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_MIN_DELAY", "3.1"))
366+
BACKOFF_MAX_DELAY = float(os.environ.get("WEBSOCKETS_BACKOFF_MAX_DELAY", "90.0"))
367+
BACKOFF_FACTOR = float(os.environ.get("WEBSOCKETS_BACKOFF_FACTOR", "1.618"))
368+
369+
370+
def backoff(
371+
initial_delay: float = BACKOFF_INITIAL_DELAY,
372+
min_delay: float = BACKOFF_MIN_DELAY,
373+
max_delay: float = BACKOFF_MAX_DELAY,
374+
factor: float = BACKOFF_FACTOR,
375+
) -> Generator[float, None, None]:
376+
"""
377+
Generate a series of backoff delays between reconnection attempts.
378+
379+
Yields:
380+
How many seconds to wait before retrying to connect.
381+
382+
"""
383+
# Add a random initial delay between 0 and 5 seconds.
384+
# See 7.2.3. Recovering from Abnormal Closure in RFC 6455.
385+
yield random.random() * initial_delay
386+
delay = min_delay
387+
while delay < max_delay:
388+
yield delay
389+
delay *= factor
390+
while True:
391+
yield max_delay

src/websockets/legacy/client.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import functools
55
import logging
6+
import os
67
import random
78
import urllib.parse
89
import warnings
@@ -591,13 +592,13 @@ def handle_redirect(self, uri: str) -> None:
591592

592593
# async for ... in connect(...):
593594

594-
BACKOFF_MIN = 1.92
595-
BACKOFF_MAX = 60.0
596-
BACKOFF_FACTOR = 1.618
597-
BACKOFF_INITIAL = 5
595+
BACKOFF_INITIAL = float(os.environ.get("WEBSOCKETS_BACKOFF_INITIAL_DELAY", "5"))
596+
BACKOFF_MIN = float(os.environ.get("WEBSOCKETS_BACKOFF_MIN_DELAY", "3.1"))
597+
BACKOFF_MAX = float(os.environ.get("WEBSOCKETS_BACKOFF_MAX_DELAY", "90.0"))
598+
BACKOFF_FACTOR = float(os.environ.get("WEBSOCKETS_BACKOFF_FACTOR", "1.618"))
598599

599600
async def __aiter__(self) -> AsyncIterator[WebSocketClientProtocol]:
600-
backoff_delay = self.BACKOFF_MIN
601+
backoff_delay = self.BACKOFF_MIN / self.BACKOFF_FACTOR
601602
while True:
602603
try:
603604
async with self as protocol:

tests/test_client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import unittest.mock
44

55
from websockets.client import *
6+
from websockets.client import backoff
67
from websockets.datastructures import Headers
78
from websockets.exceptions import InvalidHandshake, InvalidHeader
89
from websockets.frames import OP_TEXT, Frame
@@ -613,3 +614,15 @@ def test_client_connection_class(self):
613614
client = ClientConnection("ws://localhost/")
614615

615616
self.assertIsInstance(client, ClientProtocol)
617+
618+
619+
class BackoffTests(unittest.TestCase):
620+
def test_backoff(self):
621+
backoff_gen = backoff()
622+
623+
initial_delay = next(backoff_gen)
624+
self.assertGreaterEqual(initial_delay, 0)
625+
self.assertLess(initial_delay, 5)
626+
627+
following_delays = [int(next(backoff_gen)) for _ in range(9)]
628+
self.assertEqual(following_delays, [3, 5, 8, 13, 21, 34, 55, 89, 90])

0 commit comments

Comments
 (0)