Skip to content

Commit 67b5ec2

Browse files
tmunzer-AIDEclaude
andauthored
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>
1 parent 92b9938 commit 67b5ec2

12 files changed

Lines changed: 1270 additions & 188 deletions

File tree

CHANGELOG.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,47 @@
11
# 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.
42+
43+
---
44+
245
## Version 0.63.2 (July 2026)
346

447
**Released**: July 14, 2026

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -583,19 +583,22 @@ All channel classes accept the following optional keyword arguments:
583583

584584
| Parameter | Type | Default | Description |
585585
|-----------|------|---------|-------------|
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`. |
588588
| `auto_reconnect` | `bool` | `False` | Automatically reconnect on transient failures using exponential backoff. |
589589
| `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. |
592595

593596
```python
594597
ws = mistapi.websockets.sites.DeviceStatsEvents(
595598
apisession,
596599
site_ids=["<site_id>"],
597600
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
599602
auto_reconnect=True, # reconnect on transient failures
600603
)
601604
ws.connect()
@@ -609,6 +612,8 @@ ws.connect()
609612
| `ws.on_message(cb)` | `cb(data: dict)` | Register callback for incoming messages. Mutually exclusive with `receive()`. |
610613
| `ws.on_error(cb)` | `cb(error: Exception)` | Register callback for WebSocket errors |
611614
| `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. |
612617
| `ws.connect(run_in_background)` | | Open the connection. `True` (default) runs in a daemon thread; `False` blocks. |
613618
| `ws.disconnect(wait, timeout)` | | Close the connection. `wait=True` blocks until the background thread finishes. |
614619
| `ws.receive()` | `-> Generator[dict]` | Blocking generator yielding messages. Mutually exclusive with `on_message`. |

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "mistapi"
7-
version = "0.63.2"
7+
version = "0.63.3"
88
authors = [{ name = "Thomas Munzer", email = "tmunzer@juniper.net" }]
99
description = "Python package to simplify the Mist System APIs usage"
1010
keywords = ["Mist", "Juniper", "API"]

src/mistapi/__version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = "0.63.2"
1+
__version__ = "0.63.3"
22
__author__ = "Thomas Munzer <tmunzer@juniper.net>"

src/mistapi/api/v1/sites/sle.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
@deprecation.deprecated(
1919
deprecated_in="0.59.2",
2020
removed_in="0.65.0",
21-
current_version="0.63.2",
21+
current_version="0.63.3",
2222
details="function replaced with getSiteSleClassifierSummaryTrend",
2323
)
2424
def getSiteSleClassifierDetails(
@@ -764,7 +764,7 @@ def listSiteSleImpactedWirelessClients(
764764
@deprecation.deprecated(
765765
deprecated_in="0.59.2",
766766
removed_in="0.65.0",
767-
current_version="0.63.2",
767+
current_version="0.63.3",
768768
details="function replaced with getSiteSleSummaryTrend",
769769
)
770770
def getSiteSleSummary(

0 commit comments

Comments
 (0)