Skip to content

Commit 205c1a8

Browse files
authored
Merge pull request #43 from yozik04/ws_retry
Retry ws requests 4 additional times
2 parents 9e63c9e + 387ddf9 commit 205c1a8

File tree

3 files changed

+88
-12
lines changed

3 files changed

+88
-12
lines changed

tests/test_client.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import asyncio
12
import binascii
2-
from unittest.mock import AsyncMock
3+
from unittest.mock import AsyncMock, patch
34

45
import pytest
6+
import websockets
57
from websockets.exceptions import InvalidMessage
68

79
from vallox_websocket_api.client import Client
@@ -110,3 +112,43 @@ async def test_set_new_settable_address_by_address_exception(client: Client, ws)
110112

111113
with pytest.raises(ValloxWebsocketException):
112114
await client.set_values({"A_CYC_RH_VALUE": 22})
115+
116+
assert ws.send.call_count == 5
117+
118+
119+
async def test_connection_closed_ws_exception(client: Client, ws):
120+
ws.recv.side_effect = AsyncMock(side_effect=websockets.ConnectionClosed(None, None))
121+
122+
with pytest.raises(ValloxWebsocketException):
123+
await client.fetch_metric("A_CYC_ENABLED")
124+
125+
assert ws.send.call_count == 5
126+
127+
128+
async def test_ws_recv_timeout_exception(client: Client, ws):
129+
ws.recv.side_effect = AsyncMock(side_effect=asyncio.TimeoutError())
130+
131+
with pytest.raises(ValloxWebsocketException):
132+
await client.fetch_metric("A_CYC_ENABLED")
133+
134+
assert ws.send.call_count == 5
135+
136+
137+
async def test_invalid_ws_url_exception(client: Client):
138+
with patch("websockets.connect") as connect:
139+
connect.side_effect = websockets.InvalidURI("test", "test")
140+
141+
with pytest.raises(ValloxWebsocketException):
142+
await client.fetch_metric("A_CYC_ENABLED")
143+
144+
assert connect.call_count == 1
145+
146+
147+
async def test_ws_connection_timeout_exception(client: Client):
148+
with patch("websockets.connect") as connect:
149+
connect.side_effect = asyncio.TimeoutError()
150+
151+
with pytest.raises(ValloxWebsocketException):
152+
await client.fetch_metric("A_CYC_ENABLED")
153+
154+
assert connect.call_count == 5

vallox_websocket_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
"ValloxWebsocketException",
2525
]
2626

27-
__version__ = "3.2.1"
27+
__version__ = "3.3.0"

vallox_websocket_api/client.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
from functools import wraps
3+
import logging
34
import re
45
from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union, cast
56

@@ -20,8 +21,14 @@
2021
WriteMessageRequest,
2122
)
2223

24+
logger = logging.getLogger("vallox").getChild(__name__)
25+
2326
KPageSize = 65536
2427

28+
WEBSOCKETS_OPEN_TIMEOUT = 1
29+
WEBSOCKETS_RECV_TIMEOUT = 1
30+
WEBSOCKET_RETRY_DELAYS = [0.1, 0.2, 0.5, 1]
31+
2532

2633
def calculate_offset(aIndex: int) -> int:
2734
offset = 0
@@ -146,11 +153,28 @@ def to_kelvin(value: float) -> int:
146153
FuncT = TypeVar("FuncT", bound=Callable[..., Any])
147154

148155

149-
def _websocket_exception_handler(request_fn: FuncT) -> FuncT:
156+
def _websocket_retry_wrapper(request_fn: FuncT) -> FuncT:
157+
retry_on_exceptions = (
158+
websockets.InvalidHandshake,
159+
websockets.InvalidState,
160+
websockets.WebSocketProtocolError,
161+
websockets.ConnectionClosed,
162+
OSError,
163+
asyncio.TimeoutError,
164+
)
165+
150166
@wraps(request_fn)
151167
async def wrapped(*args: Any, **kwargs: Any) -> Any:
152168
try:
153-
return await request_fn(*args, **kwargs)
169+
delays = WEBSOCKET_RETRY_DELAYS.copy()
170+
while len(delays) >= 0:
171+
try:
172+
return await request_fn(*args, **kwargs)
173+
except Exception as e:
174+
if isinstance(e, retry_on_exceptions) and len(delays) > 0:
175+
await asyncio.sleep(delays.pop(0))
176+
else:
177+
raise e
154178
except websockets.InvalidHandshake as e:
155179
raise ValloxWebsocketException("Websocket handshake failed") from e
156180
except websockets.InvalidURI as e:
@@ -161,8 +185,12 @@ async def wrapped(*args: Any, **kwargs: Any) -> Any:
161185
raise ValloxWebsocketException("Websocket invalid state") from e
162186
except websockets.WebSocketProtocolError as e:
163187
raise ValloxWebsocketException("Websocket protocol error") from e
188+
except websockets.ConnectionClosed as e:
189+
raise ValloxWebsocketException("Websocket connection closed") from e
164190
except OSError as e:
165191
raise ValloxWebsocketException("Websocket connection failed") from e
192+
except asyncio.TimeoutError as e:
193+
raise ValloxWebsocketException("Websocket connection timed out") from e
166194

167195
return cast(FuncT, wrapped)
168196

@@ -232,20 +260,26 @@ def _encode_pair(
232260

233261
return address, raw_value
234262

235-
@_websocket_exception_handler
236263
async def _websocket_request(self, payload: bytes) -> bytes:
237-
async with websockets.connect(f"ws://{self.ip_address}/") as ws:
238-
await ws.send(payload)
239-
r: bytes = await ws.recv()
240-
return r
264+
return (await self._websocket_request_multiple(payload, 1))[0]
241265

242-
@_websocket_exception_handler
266+
@_websocket_retry_wrapper
243267
async def _websocket_request_multiple(
244268
self, payload: bytes, read_packets: int
245269
) -> List[bytes]:
246-
async with websockets.connect(f"ws://{self.ip_address}/") as ws:
270+
async with websockets.connect(
271+
f"ws://{self.ip_address}/",
272+
open_timeout=WEBSOCKETS_OPEN_TIMEOUT,
273+
logger=logger,
274+
) as ws:
247275
await ws.send(payload)
248-
return await asyncio.gather(*[ws.recv() for _ in range(0, read_packets)])
276+
277+
async def _get_responses() -> List[bytes]:
278+
return [await ws.recv() for _ in range(0, read_packets)]
279+
280+
return await asyncio.wait_for(
281+
_get_responses(), timeout=WEBSOCKETS_RECV_TIMEOUT * read_packets
282+
)
249283

250284
async def fetch_metrics(
251285
self, metric_keys: Optional[List[str]] = None

0 commit comments

Comments
 (0)