You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Improve websocket reliability per Mist best practices (#26)
* Improve websocket reliability per Mist best practices
* Address PR feedback for websocket reliability changes
* Tune websocket logging and metrics thread safety
* fix(websockets): address review findings on reliability rework
- derive ping_timeout as min(45, ping_interval - 1) when not supplied, so
existing callers passing only ping_interval keep working
- drain the callback queue on shutdown instead of dropping pending
messages; the None sentinel now marks end-of-stream
- report subscription watchdog timeouts and subscribe_failed events
through on_error so callers without auto_reconnect get a signal
- don't resurrect the callback worker after disconnect (stop flag is now
only cleared by connect())
- cite the Mist rate-limits doc for the channel-count constants
- document the new reliability kwargs in all channel class docstrings
- add tests: ping derivation, worker drain, worker stop guard, watchdog
expiry/cancel, subscribe_failed error reporting
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(websockets): clarify ping_timeout derivation and queue_maxsize scope
- note that ping_timeout falls back to 45 when ping_interval=0 (pings
disabled, value unused) in the constants comment, README, and all
channel class docstrings
- document that queue_maxsize bounds both the receive() and callback
queues in the channel class docstrings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(websockets): reset reconnect state after subscriptions
* docs(changelog): add 0.63.3 websocket reliability notes
* fix: update version to 0.63.3 in pyproject.toml
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+43Lines changed: 43 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,4 +1,47 @@
1
1
# CHANGELOG
2
+
3
+
## Version 0.63.3 (July 2026)
4
+
5
+
**Released**: July 14, 2026
6
+
7
+
This patch release improves WebSocket subscription reliability, isolates user callbacks from the receive loop, and adds stronger reconnect, rate-limit, and connection-health safeguards. (#26)
8
+
9
+
---
10
+
11
+
### 1. NEW FEATURES
12
+
13
+
#### **Subscription Acknowledgement Watchdog**
14
+
WebSocket clients now track `channel_subscribed` acknowledgements for every requested channel. The configurable `subscription_watchdog_timeout` closes an incomplete connection and reports a `TimeoutError` through `on_error`; when `auto_reconnect=True`, the client reconnects and retries the subscriptions. Explicit `subscribe_failed` events are also surfaced through `on_error` before reconnecting.
15
+
16
+
#### **Ping and Pong Hooks**
17
+
Added `on_ping()` and `on_pong()` callbacks for applications that need visibility into WebSocket keepalive frames.
18
+
19
+
---
20
+
21
+
### 2. IMPROVEMENTS
22
+
23
+
#### **Non-Blocking Callback Delivery**
24
+
Incoming frames are now placed on a dedicated callback-worker queue so slow user callbacks no longer block the WebSocket receive loop. Pending callback messages are drained during shutdown, and `queue_maxsize` bounds both generator and callback delivery queues.
25
+
26
+
#### **Reconnect and Rate-Limit Handling**
27
+
- HTTP 429 handshake failures use the configurable `rate_limit_backoff` as a minimum reconnect delay.
28
+
- Exponential reconnect state is reset only after all requested subscriptions are acknowledged, so persistent subscription failures respect `max_reconnect_attempts` and continue increasing the backoff.
29
+
-`SessionWithUrl` connections with no managed channels reset reconnect state when the transport opens.
30
+
31
+
#### **Connection Health and Observability**
32
+
- Updated keepalive defaults to `ping_interval=60` and a derived `ping_timeout` of up to 45 seconds.
33
+
- Added periodic, thread-safe throughput and queue-depth logging through `throughput_log_interval`.
34
+
- Duplicate channels are removed while preserving order, high channel counts produce a warning, and connections above the 2,000-channel limit are rejected.
35
+
36
+
---
37
+
38
+
### 3. BUG FIXES
39
+
40
+
#### **Subscription Completion Accounting**
41
+
Unexpected `channel_subscribed` acknowledgements are now ignored before updating subscription progress, preventing them from cancelling the watchdog while requested channels are still missing.
Copy file name to clipboardExpand all lines: README.md
+10-5Lines changed: 10 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -583,19 +583,22 @@ All channel classes accept the following optional keyword arguments:
583
583
584
584
| Parameter | Type | Default | Description |
585
585
|-----------|------|---------|-------------|
586
-
|`ping_interval`|`int`|`30`| Seconds between automatic ping frames. Set to `0` to disable pings. |
587
-
|`ping_timeout`|`int`|`10`| Seconds to wait for a pong response before treating the connection as dead. |
586
+
|`ping_interval`|`int`|`60`| Seconds between automatic ping frames. Set to `0` to disable pings. |
587
+
|`ping_timeout`|`int \| None`|`None`| Seconds to wait for a pong response before treating the connection as dead. Defaults to `min(45, ping_interval - 1)` when pings are enabled, or `45` when `ping_interval=0` (unused since pings are disabled). When `ping_interval > 0`, this must be lower than `ping_interval`. |
588
588
|`auto_reconnect`|`bool`|`False`| Automatically reconnect on transient failures using exponential backoff. |
589
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. |
591
-
|`queue_maxsize`|`int`|`0`| Maximum messages buffered in the internal queue for `receive()`. `0` means unbounded. When set, incoming messages are dropped with a warning when the queue is full, preventing memory growth on high-frequency streams. |
590
+
|`reconnect_backoff`|`float`|`2.0`| Base backoff delay in seconds. Doubles after each failed attempt (2s, 4s, 8s, ...). Resets once the connection is fully established and all requested subscriptions are acknowledged. |
591
+
|`queue_maxsize`|`int`|`0`| Maximum messages buffered in the internal queues used for both `receive()` and callback delivery. `0` means unbounded. When set, incoming messages are dropped with a warning when either queue is full, preventing memory growth on high-frequency streams. |
592
+
|`subscription_watchdog_timeout`|`float`|`10.0`| Maximum time to wait for all `channel_subscribed` acknowledgements after connect. On timeout, the error is reported to `on_error` and the connection is closed; with `auto_reconnect=True` this triggers a clean reconnect. |
593
+
|`rate_limit_backoff`|`float`|`30.0`| Minimum reconnect delay after a 429 rate-limit response. |
594
+
|`throughput_log_interval`|`int`|`100`| Logs queue depth and processed counts every N messages. Set to `0` to disable periodic throughput logs. |
592
595
593
596
```python
594
597
ws = mistapi.websockets.sites.DeviceStatsEvents(
595
598
apisession,
596
599
site_ids=["<site_id>"],
597
600
ping_interval=60, # ping every 60 s
598
-
ping_timeout=20, # wait up to 20 s for pong
601
+
ping_timeout=45, # wait up to 45 s for pong
599
602
auto_reconnect=True, # reconnect on transient failures
600
603
)
601
604
ws.connect()
@@ -609,6 +612,8 @@ ws.connect()
609
612
|`ws.on_message(cb)`|`cb(data: dict)`| Register callback for incoming messages. Mutually exclusive with `receive()`. |
610
613
|`ws.on_error(cb)`|`cb(error: Exception)`| Register callback for WebSocket errors |
611
614
|`ws.on_close(cb)`|`cb(code: int \| None, msg: str \| None)`| Register callback for connection close. Safe to call `connect()` from within. |
615
+
|`ws.on_ping(cb)`|`cb(message: str \| bytes \| None)`| Register callback for received ping frames. |
616
+
|`ws.on_pong(cb)`|`cb(message: str \| bytes \| None)`| Register callback for received pong frames. |
612
617
|`ws.connect(run_in_background)`|| Open the connection. `True` (default) runs in a daemon thread; `False` blocks. |
613
618
|`ws.disconnect(wait, timeout)`|| Close the connection. `wait=True` blocks until the background thread finishes. |
0 commit comments