Opt-in auto-reconnect (backoff) for stream_measurements #49

Merged
mat merged 2 commits from feat/streaming-reconnect into main 2026-07-13 14:49:42 +00:00
Owner

Summary

Slice 2/2 of the streaming work: adds opt-in auto-reconnect with exponential backoff to ComwattClient.stream_measurements. Stacked on #48 (base branch feat/streaming-websocket) — review/merge #48 first.

New keyword-only params (default keeps slice-1 behavior exactly):

  • reconnect: bool = False — opt-in; False = single connection, generator stops on drop (unchanged).
  • reconnect_backoff=1.0, reconnect_backoff_max=60.0 — exponential (×2) delay with full jitter, capped.
  • reconnect_max_attempts: int | None = None — max consecutive failed connects before giving up (None = unlimited); reset after any successful (re)connection.

Behavior when reconnect=True: on a dropped socket or a connection failure, sleep (jittered backoff) then re-open → re-CONNECT → re-SUBSCRIBE (siteUid topic) → re-SEND /app/streaming/start, and keep yielding transparently. Backoff + attempt counter reset after each successful session. An auth rejection is terminal: at most one _reauthenticate() retry (when auto_reauth + stored creds), else ComwattAuthError — never loops on bad credentials. The current cwt_session cookie is re-read on every attempt (so a mid-stream re-auth is honored).

Also hardens the connect path (applies to both modes): connection and STOMP-handshake WebSocketExceptions are wrapped in ComwattStreamingError (no raw library exceptions escape), and the socket is always closed on any connect/handshake failure.

Design

Refactored the slice-1 monolith into _connect_and_start (open + handshake + subscribe + start, returns a ready socket or raises a typed error, always closes on failure) and _consume_stream (the recv→yield loop). The public method is a thin dispatcher: single-shot when reconnect=False, a backoff retry loop when True. Sync only; no async/threads/callbacks; reconnection is transparent (no new event types).

Test plan

  • Full suite 128 passed (116 pre-existing + 12 streaming reconnect tests), output pristine (filterwarnings=error).
  • mypy comwatt_client clean.
  • Backward compat: reconnect is keyword-only, default False; the non-reconnect path is behaviorally identical to #48; no pre-existing test modified.
  • Covered: transparent resume across reconnects (event order), exponential backoff + cap (exact sleep args), give-up after N attempts, connection-error + STOMP-handshake-error wrapping (+ socket closed), auth refresh once on reconnect, auth-terminal (≤2 opens then ComwattAuthError), fresh cookie after reauth, backoff reset after a good session, DISCONNECT/close cleanup under reconnect + GeneratorExit.

Notes

  • Draft, and stacked on #48 — merge #48 first, then this retargets cleanly to main.
  • Went through SDD review: an initial review caught a Critical (unwrapped STOMP-handshake errors escaping the reconnect loop + a socket leak) and an Important (stale cookie across iterations); both fixed and re-review approved.
## Summary Slice 2/2 of the streaming work: adds **opt-in auto-reconnect with exponential backoff** to `ComwattClient.stream_measurements`. **Stacked on #48** (base branch `feat/streaming-websocket`) — review/merge #48 first. New keyword-only params (default keeps slice-1 behavior exactly): - `reconnect: bool = False` — opt-in; `False` = single connection, generator stops on drop (unchanged). - `reconnect_backoff=1.0`, `reconnect_backoff_max=60.0` — exponential (×2) delay with full jitter, capped. - `reconnect_max_attempts: int | None = None` — max *consecutive* failed connects before giving up (`None` = unlimited); reset after any successful (re)connection. Behavior when `reconnect=True`: on a dropped socket or a connection failure, sleep (jittered backoff) then re-open → re-CONNECT → re-SUBSCRIBE (siteUid topic) → re-SEND `/app/streaming/start`, and keep yielding transparently. Backoff + attempt counter reset after each successful session. An **auth rejection is terminal**: at most one `_reauthenticate()` retry (when `auto_reauth` + stored creds), else `ComwattAuthError` — never loops on bad credentials. The current `cwt_session` cookie is re-read on every attempt (so a mid-stream re-auth is honored). Also hardens the connect path (applies to both modes): connection and STOMP-handshake `WebSocketException`s are wrapped in `ComwattStreamingError` (no raw library exceptions escape), and the socket is always closed on any connect/handshake failure. ## Design Refactored the slice-1 monolith into `_connect_and_start` (open + handshake + subscribe + start, returns a ready socket or raises a typed error, always closes on failure) and `_consume_stream` (the recv→yield loop). The public method is a thin dispatcher: single-shot when `reconnect=False`, a backoff retry loop when `True`. Sync only; no async/threads/callbacks; reconnection is transparent (no new event types). ## Test plan - [x] Full suite **128 passed** (116 pre-existing + 12 streaming reconnect tests), output pristine (`filterwarnings=error`). - [x] `mypy comwatt_client` clean. - [x] Backward compat: `reconnect` is keyword-only, default `False`; the non-reconnect path is behaviorally identical to #48; no pre-existing test modified. - [x] Covered: transparent resume across reconnects (event order), exponential backoff + cap (exact `sleep` args), give-up after N attempts, connection-error + STOMP-handshake-error wrapping (+ socket closed), auth refresh once on reconnect, auth-terminal (≤2 opens then `ComwattAuthError`), fresh cookie after reauth, backoff reset after a good session, DISCONNECT/close cleanup under reconnect + `GeneratorExit`. ## Notes - **Draft**, and stacked on #48 — merge #48 first, then this retargets cleanly to `main`. - Went through SDD review: an initial review caught a Critical (unwrapped STOMP-handshake errors escaping the reconnect loop + a socket leak) and an Important (stale cookie across iterations); both fixed and re-review approved.
- Extract _connect_and_start() method: opens WS, handles auth refresh
  on 401/403 (one retry after _reauthenticate()), wraps other
  WebSocketException into ComwattStreamingError
- Extract _consume_stream() module-level function: receive loop only,
  no socket lifecycle
- Add stream_measurements() keyword params: reconnect, reconnect_backoff,
  reconnect_backoff_max, reconnect_max_attempts
- reconnect=False (default): identical to slice-1 behavior, plus
  connection errors now surface as ComwattStreamingError
- reconnect=True: full-jitter exponential backoff loop; resets delay and
  attempt counter after any successful session; ComwattAuthError is
  terminal; gives up after reconnect_max_attempts consecutive failures
- Import time and random at module level for test monkeypatching
- Add 9 TDD tests covering: backward compat, transparent resume,
  backoff cap, give-up, error wrapping, auth refresh, auth terminal,
  backoff reset, cleanup under reconnect
- Update README: Features bullet and Realtime streaming section
🐛 fix: harden STOMP handshake, freshen cookie on reconnect, tighten assertions
All checks were successful
Tests / forgejo-pytest (pull_request) Successful in 43s
Tests / forgejo-pytest (push) Successful in 43s
6e8ad04ab1
mat changed target branch from feat/streaming-websocket to main 2026-07-13 14:00:01 +00:00
mat force-pushed feat/streaming-reconnect from 6e8ad04ab1
All checks were successful
Tests / forgejo-pytest (pull_request) Successful in 43s
Tests / forgejo-pytest (push) Successful in 43s
to 4daf185414
All checks were successful
Tests / forgejo-pytest (push) Successful in 42s
Tests / forgejo-pytest (pull_request) Successful in 43s
2026-07-13 14:36:16 +00:00
Compare
mat changed title from WIP: Opt-in auto-reconnect (backoff) for stream_measurements to Opt-in auto-reconnect (backoff) for stream_measurements 2026-07-13 14:37:41 +00:00
mat merged commit 4a581fe33a into main 2026-07-13 14:49:42 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mat/python-comwatt-client!49
No description provided.