Skip to content

Commit 92ff338

Browse files
committed
propagate ws reconnect to other classes
1 parent b0aa5f9 commit 92ff338

7 files changed

Lines changed: 190 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ Updated to mist_openapi spec version 2602.1.7.
4747
---
4848

4949
### 3. BUG FIXES
50-
5150
- Fixed `ShellSession.recv()` to gracefully handle socket timeout reset when the connection is already closed
51+
- Fixed thread-safety (TOCTOU) race conditions in `ShellSession` by capturing WebSocket reference in local variables across `disconnect()`, `connected`, `send()`, `recv()`, and `resize()` methods
52+
- Fixed thread-safety race condition in `_MistWebsocket.disconnect()` with local variable capture
5253

5354
---
5455

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -579,19 +579,23 @@ The package provides a WebSocket client for real-time event streaming from the M
579579

580580
### Connection Parameters
581581

582-
All channel classes accept the following optional keyword arguments to control the WebSocket keep-alive behaviour:
582+
All channel classes accept the following optional keyword arguments:
583583

584584
| Parameter | Type | Default | Description |
585585
|-----------|------|---------|-------------|
586586
| `ping_interval` | `int` | `30` | Seconds between automatic ping frames. Set to `0` to disable pings. |
587587
| `ping_timeout` | `int` | `10` | Seconds to wait for a pong response before treating the connection as dead. |
588+
| `auto_reconnect` | `bool` | `False` | Automatically reconnect on transient failures using exponential backoff. |
589+
| `max_reconnect_attempts` | `int` | `5` | Maximum number of reconnect attempts before giving up. |
590+
| `reconnect_backoff` | `float` | `2.0` | Base backoff delay in seconds. Doubles after each failed attempt (2s, 4s, 8s, ...). Resets on successful reconnection. |
588591

589592
```python
590593
ws = mistapi.websockets.sites.DeviceStatsEvents(
591594
apisession,
592595
site_ids=["<site_id>"],
593-
ping_interval=60, # ping every 60 s
594-
ping_timeout=20, # wait up to 20 s for pong
596+
ping_interval=60, # ping every 60 s
597+
ping_timeout=20, # wait up to 20 s for pong
598+
auto_reconnect=True, # reconnect on transient failures
595599
)
596600
ws.connect()
597601
```

src/mistapi/websockets/location.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ class BleAssetsEvents(_MistWebsocket):
3333
Interval in seconds to send WebSocket ping frames (keep-alive).
3434
ping_timeout : int, default 10
3535
Time in seconds to wait for a ping response before considering the connection dead.
36+
auto_reconnect : bool, default False
37+
Automatically reconnect on transient failures using exponential backoff.
38+
max_reconnect_attempts : int, default 5
39+
Maximum number of reconnect attempts before giving up.
40+
reconnect_backoff : float, default 2.0
41+
Base backoff delay in seconds. Doubles after each failed attempt.
3642
3743
3844
EXAMPLE
@@ -67,13 +73,19 @@ def __init__(
6773
map_id: list[str],
6874
ping_interval: int = 30,
6975
ping_timeout: int = 10,
76+
auto_reconnect: bool = False,
77+
max_reconnect_attempts: int = 5,
78+
reconnect_backoff: float = 2.0,
7079
) -> None:
7180
channels = [f"/sites/{site_id}/stats/maps/{mid}/assets" for mid in map_id]
7281
super().__init__(
7382
mist_session,
7483
channels=channels,
7584
ping_interval=ping_interval,
7685
ping_timeout=ping_timeout,
86+
auto_reconnect=auto_reconnect,
87+
max_reconnect_attempts=max_reconnect_attempts,
88+
reconnect_backoff=reconnect_backoff,
7789
)
7890

7991

@@ -128,13 +140,19 @@ def __init__(
128140
map_id: list[str],
129141
ping_interval: int = 30,
130142
ping_timeout: int = 10,
143+
auto_reconnect: bool = False,
144+
max_reconnect_attempts: int = 5,
145+
reconnect_backoff: float = 2.0,
131146
) -> None:
132147
channels = [f"/sites/{site_id}/stats/maps/{mid}/clients" for mid in map_id]
133148
super().__init__(
134149
mist_session,
135150
channels=channels,
136151
ping_interval=ping_interval,
137152
ping_timeout=ping_timeout,
153+
auto_reconnect=auto_reconnect,
154+
max_reconnect_attempts=max_reconnect_attempts,
155+
reconnect_backoff=reconnect_backoff,
138156
)
139157

140158

@@ -189,13 +207,19 @@ def __init__(
189207
map_id: list[str],
190208
ping_interval: int = 30,
191209
ping_timeout: int = 10,
210+
auto_reconnect: bool = False,
211+
max_reconnect_attempts: int = 5,
212+
reconnect_backoff: float = 2.0,
192213
) -> None:
193214
channels = [f"/sites/{site_id}/stats/maps/{mid}/sdkclients" for mid in map_id]
194215
super().__init__(
195216
mist_session,
196217
channels=channels,
197218
ping_interval=ping_interval,
198219
ping_timeout=ping_timeout,
220+
auto_reconnect=auto_reconnect,
221+
max_reconnect_attempts=max_reconnect_attempts,
222+
reconnect_backoff=reconnect_backoff,
199223
)
200224

201225

@@ -250,6 +274,9 @@ def __init__(
250274
map_id: list[str],
251275
ping_interval: int = 30,
252276
ping_timeout: int = 10,
277+
auto_reconnect: bool = False,
278+
max_reconnect_attempts: int = 5,
279+
reconnect_backoff: float = 2.0,
253280
) -> None:
254281
channels = [
255282
f"/sites/{site_id}/stats/maps/{mid}/unconnected_clients" for mid in map_id
@@ -259,6 +286,9 @@ def __init__(
259286
channels=channels,
260287
ping_interval=ping_interval,
261288
ping_timeout=ping_timeout,
289+
auto_reconnect=auto_reconnect,
290+
max_reconnect_attempts=max_reconnect_attempts,
291+
reconnect_backoff=reconnect_backoff,
262292
)
263293

264294

@@ -313,6 +343,9 @@ def __init__(
313343
map_id: list[str],
314344
ping_interval: int = 30,
315345
ping_timeout: int = 10,
346+
auto_reconnect: bool = False,
347+
max_reconnect_attempts: int = 5,
348+
reconnect_backoff: float = 2.0,
316349
) -> None:
317350
channels = [
318351
f"/sites/{site_id}/stats/maps/{mid}/discovered_assets" for mid in map_id
@@ -322,4 +355,7 @@ def __init__(
322355
channels=channels,
323356
ping_interval=ping_interval,
324357
ping_timeout=ping_timeout,
358+
auto_reconnect=auto_reconnect,
359+
max_reconnect_attempts=max_reconnect_attempts,
360+
reconnect_backoff=reconnect_backoff,
325361
)

src/mistapi/websockets/orgs.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ class InsightsEvents(_MistWebsocket):
3131
Interval in seconds to send WebSocket ping frames (keep-alive).
3232
ping_timeout : int, default 10
3333
Time in seconds to wait for a ping response before considering the connection dead.
34+
auto_reconnect : bool, default False
35+
Automatically reconnect on transient failures using exponential backoff.
36+
max_reconnect_attempts : int, default 5
37+
Maximum number of reconnect attempts before giving up.
38+
reconnect_backoff : float, default 2.0
39+
Base backoff delay in seconds. Doubles after each failed attempt.
3440
3541
EXAMPLE
3642
-----------
@@ -63,12 +69,18 @@ def __init__(
6369
org_id: str,
6470
ping_interval: int = 30,
6571
ping_timeout: int = 10,
72+
auto_reconnect: bool = False,
73+
max_reconnect_attempts: int = 5,
74+
reconnect_backoff: float = 2.0,
6675
) -> None:
6776
super().__init__(
6877
mist_session,
6978
channels=[f"/orgs/{org_id}/insights/summary"],
7079
ping_interval=ping_interval,
7180
ping_timeout=ping_timeout,
81+
auto_reconnect=auto_reconnect,
82+
max_reconnect_attempts=max_reconnect_attempts,
83+
reconnect_backoff=reconnect_backoff,
7284
)
7385

7486

@@ -88,6 +100,12 @@ class MxEdgesStatsEvents(_MistWebsocket):
88100
Interval in seconds to send WebSocket ping frames (keep-alive).
89101
ping_timeout : int, default 10
90102
Time in seconds to wait for a ping response before considering the connection dead.
103+
auto_reconnect : bool, default False
104+
Automatically reconnect on transient failures using exponential backoff.
105+
max_reconnect_attempts : int, default 5
106+
Maximum number of reconnect attempts before giving up.
107+
reconnect_backoff : float, default 2.0
108+
Base backoff delay in seconds. Doubles after each failed attempt.
91109
92110
EXAMPLE
93111
-----------
@@ -120,12 +138,18 @@ def __init__(
120138
org_id: str,
121139
ping_interval: int = 30,
122140
ping_timeout: int = 10,
141+
auto_reconnect: bool = False,
142+
max_reconnect_attempts: int = 5,
143+
reconnect_backoff: float = 2.0,
123144
) -> None:
124145
super().__init__(
125146
mist_session,
126147
channels=[f"/orgs/{org_id}/stats/mxedges"],
127148
ping_interval=ping_interval,
128149
ping_timeout=ping_timeout,
150+
auto_reconnect=auto_reconnect,
151+
max_reconnect_attempts=max_reconnect_attempts,
152+
reconnect_backoff=reconnect_backoff,
129153
)
130154

131155

@@ -145,6 +169,12 @@ class MxEdgesEvents(_MistWebsocket):
145169
Interval in seconds to send WebSocket ping frames (keep-alive).
146170
ping_timeout : int, default 10
147171
Time in seconds to wait for a ping response before considering the connection dead.
172+
auto_reconnect : bool, default False
173+
Automatically reconnect on transient failures using exponential backoff.
174+
max_reconnect_attempts : int, default 5
175+
Maximum number of reconnect attempts before giving up.
176+
reconnect_backoff : float, default 2.0
177+
Base backoff delay in seconds. Doubles after each failed attempt.
148178
149179
EXAMPLE
150180
-----------
@@ -177,10 +207,16 @@ def __init__(
177207
org_id: str,
178208
ping_interval: int = 30,
179209
ping_timeout: int = 10,
210+
auto_reconnect: bool = False,
211+
max_reconnect_attempts: int = 5,
212+
reconnect_backoff: float = 2.0,
180213
) -> None:
181214
super().__init__(
182215
mist_session,
183216
channels=[f"/orgs/{org_id}/mxedges"],
184217
ping_interval=ping_interval,
185218
ping_timeout=ping_timeout,
219+
auto_reconnect=auto_reconnect,
220+
max_reconnect_attempts=max_reconnect_attempts,
221+
reconnect_backoff=reconnect_backoff,
186222
)

src/mistapi/websockets/session.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ class SessionWithUrl(_MistWebsocket):
3232
Interval in seconds to send WebSocket ping frames (keep-alive).
3333
ping_timeout : int, default 10
3434
Time in seconds to wait for a ping response before considering the connection dead.
35+
auto_reconnect : bool, default False
36+
Automatically reconnect on transient failures using exponential backoff.
37+
max_reconnect_attempts : int, default 5
38+
Maximum number of reconnect attempts before giving up.
39+
reconnect_backoff : float, default 2.0
40+
Base backoff delay in seconds. Doubles after each failed attempt.
3541
3642
EXAMPLE
3743
-----------
@@ -64,13 +70,19 @@ def __init__(
6470
url: str,
6571
ping_interval: int = 30,
6672
ping_timeout: int = 10,
73+
auto_reconnect: bool = False,
74+
max_reconnect_attempts: int = 5,
75+
reconnect_backoff: float = 2.0,
6776
) -> None:
6877
self._url = url
6978
super().__init__(
7079
mist_session,
7180
channels=[],
7281
ping_interval=ping_interval,
7382
ping_timeout=ping_timeout,
83+
auto_reconnect=auto_reconnect,
84+
max_reconnect_attempts=max_reconnect_attempts,
85+
reconnect_backoff=reconnect_backoff,
7486
)
7587

7688
def _build_ws_url(self) -> str:

0 commit comments

Comments
 (0)