Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,47 @@
# CHANGELOG

## Version 0.63.3 (July 2026)

**Released**: July 14, 2026

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)

---

### 1. NEW FEATURES

#### **Subscription Acknowledgement Watchdog**
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.

#### **Ping and Pong Hooks**
Added `on_ping()` and `on_pong()` callbacks for applications that need visibility into WebSocket keepalive frames.

---

### 2. IMPROVEMENTS

#### **Non-Blocking Callback Delivery**
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.

#### **Reconnect and Rate-Limit Handling**
- HTTP 429 handshake failures use the configurable `rate_limit_backoff` as a minimum reconnect delay.
- 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.
- `SessionWithUrl` connections with no managed channels reset reconnect state when the transport opens.

#### **Connection Health and Observability**
- Updated keepalive defaults to `ping_interval=60` and a derived `ping_timeout` of up to 45 seconds.
- Added periodic, thread-safe throughput and queue-depth logging through `throughput_log_interval`.
- Duplicate channels are removed while preserving order, high channel counts produce a warning, and connections above the 2,000-channel limit are rejected.

---

### 3. BUG FIXES

#### **Subscription Completion Accounting**
Unexpected `channel_subscribed` acknowledgements are now ignored before updating subscription progress, preventing them from cancelling the watchdog while requested channels are still missing.

---

## Version 0.63.2 (July 2026)

**Released**: July 14, 2026
Expand Down
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,19 +583,22 @@ All channel classes accept the following optional keyword arguments:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ping_interval` | `int` | `30` | Seconds between automatic ping frames. Set to `0` to disable pings. |
| `ping_timeout` | `int` | `10` | Seconds to wait for a pong response before treating the connection as dead. |
| `ping_interval` | `int` | `60` | Seconds between automatic ping frames. Set to `0` to disable pings. |
| `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`. |
| `auto_reconnect` | `bool` | `False` | Automatically reconnect on transient failures using exponential backoff. |
| `max_reconnect_attempts` | `int` | `5` | Maximum number of reconnect attempts before giving up. |
| `reconnect_backoff` | `float` | `2.0` | Base backoff delay in seconds. Doubles after each failed attempt (2s, 4s, 8s, ...). Resets on successful reconnection. |
| `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. |
| `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. |
| `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. |
| `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. |
| `rate_limit_backoff` | `float` | `30.0` | Minimum reconnect delay after a 429 rate-limit response. |
| `throughput_log_interval` | `int` | `100` | Logs queue depth and processed counts every N messages. Set to `0` to disable periodic throughput logs. |
Comment thread
tmunzer-AIDE marked this conversation as resolved.

```python
ws = mistapi.websockets.sites.DeviceStatsEvents(
apisession,
site_ids=["<site_id>"],
ping_interval=60, # ping every 60 s
ping_timeout=20, # wait up to 20 s for pong
ping_timeout=45, # wait up to 45 s for pong
auto_reconnect=True, # reconnect on transient failures
)
ws.connect()
Expand All @@ -609,6 +612,8 @@ ws.connect()
| `ws.on_message(cb)` | `cb(data: dict)` | Register callback for incoming messages. Mutually exclusive with `receive()`. |
| `ws.on_error(cb)` | `cb(error: Exception)` | Register callback for WebSocket errors |
| `ws.on_close(cb)` | `cb(code: int \| None, msg: str \| None)` | Register callback for connection close. Safe to call `connect()` from within. |
| `ws.on_ping(cb)` | `cb(message: str \| bytes \| None)` | Register callback for received ping frames. |
| `ws.on_pong(cb)` | `cb(message: str \| bytes \| None)` | Register callback for received pong frames. |
| `ws.connect(run_in_background)` | | Open the connection. `True` (default) runs in a daemon thread; `False` blocks. |
| `ws.disconnect(wait, timeout)` | | Close the connection. `wait=True` blocks until the background thread finishes. |
| `ws.receive()` | `-> Generator[dict]` | Blocking generator yielding messages. Mutually exclusive with `on_message`. |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "mistapi"
version = "0.63.2"
version = "0.63.3"
authors = [{ name = "Thomas Munzer", email = "tmunzer@juniper.net" }]
description = "Python package to simplify the Mist System APIs usage"
keywords = ["Mist", "Juniper", "API"]
Expand Down
2 changes: 1 addition & 1 deletion src/mistapi/__version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = "0.63.2"
__version__ = "0.63.3"
__author__ = "Thomas Munzer <tmunzer@juniper.net>"
4 changes: 2 additions & 2 deletions src/mistapi/api/v1/sites/sle.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@deprecation.deprecated(
deprecated_in="0.59.2",
removed_in="0.65.0",
current_version="0.63.2",
current_version="0.63.3",
details="function replaced with getSiteSleClassifierSummaryTrend",
)
def getSiteSleClassifierDetails(
Expand Down Expand Up @@ -764,7 +764,7 @@ def listSiteSleImpactedWirelessClients(
@deprecation.deprecated(
deprecated_in="0.59.2",
removed_in="0.65.0",
current_version="0.63.2",
current_version="0.63.3",
details="function replaced with getSiteSleSummaryTrend",
)
def getSiteSleSummary(
Expand Down
Loading
Loading