Skip to content

fix(connection): stop reconnect storms, unbounded poll cycles and event-loop stalls - #618

Open
greggo74 wants to merge 2 commits into
ParadoxAlarmInterface:devfrom
greggo74:connection-stability-eval
Open

fix(connection): stop reconnect storms, unbounded poll cycles and event-loop stalls#618
greggo74 wants to merge 2 commits into
ParadoxAlarmInterface:devfrom
greggo74:connection-stability-eval

Conversation

@greggo74

@greggo74 greggo74 commented Sep 7, 2026

Copy link
Copy Markdown

What this is

My IP150/paradoxmyhome setup was dropping its panel connection several times a
day. Rather than patch around it locally I worked through the connection and
polling paths and ended up with 13 findings, most of which reproduce without any
hardware. This PR fixes them and adds regression tests for each.

Happy to split this into smaller PRs if that is easier to review — say the word
and I will break it up by tier.

Tier 1 — most likely to cause the drops

  1. IO_TIMEOUT from pai.conf was silently ignored. cfg.IO_TIMEOUT was
    captured into default arguments at import time, so the configured value never
    reached any request path. Anyone who raised it to work around timeouts has
    been running the default all along.
  2. The poll cycle had no upper bound and degraded into a poll storm once
    replies started arriving late. Now bounded by Panel.status_cycle_budget; a
    cycle that exceeds it is cancelled and counted as a missing reply, so it
    surfaces as "Replies missing" and a reconnect instead of silence.
  3. Reconnect backoff used 2 ^ retry — bitwise XOR, not exponentiation.
    The real sequence was 3, 0, 1, 6, 7… so the second retry fired immediately.
    Now 2, 4, 8, 16, 30, 30 s.
  4. Failed IP connection attempts leaked the previous socket, holding open the
    IP150's single session slot so the retry could not get in.

Tier 2 — STUN / paradoxmyhome path

  1. refresh_session_if_required() did blocking socket I/O on the event loop.
  2. time.sleep(5) inside an async function, plus an unbounded HTTP call.
  3. STUN response parsing assumed one recv() returns a whole message.
  4. A failed STUN refresh did not mark the connection dead.

Tier 3 — smaller, still real

  1. busy.release() could be called without holding the lock.
  2. asyncio.gather abandoned sibling requests on first failure.
  3. A failed close left the connection half-torn-down.
  4. A status parse failure crashed the merge instead of counting as a missing reply.
  5. disconnect() could construct a connection object during shutdown.

Behaviour changes worth knowing about

  • Reconnect backoff is slower for a brief blip, far more reliable for a real
    outage, because PAI stops hammering the module.
  • IP connect attempts are now 5 s apart (CONNECT_RETRY_DELAY), so a failing
    connect() takes ~10 s longer before handing back to the main retry loop.
  • Raising IO_TIMEOUT now actually takes effect — existing configs should
    re-check their value.
  • A failed IP attempt now closes its STUN session, so the next attempt re-fetches
    SWAN site info instead of reusing a possibly stale xoraddr. Costs one extra
    HTTPS round trip per retry.
  • PRT3 overrides the cycle budget, because its single virtual address expands
    into one request per area and zone.

Testing

21 new regression tests across tests/connection/, tests/lib/,
tests/paradox/ and tests/test_main_uptime.py. Full suite for the touched
areas: 319 passed. Each finding has a test that fails before its fix.

Note on CONNECTION_STABILITY_REVIEW.md

The first commit adds the full write-up as a document in the repo root, with the
reasoning and file/line references behind each finding. I have kept it because it
makes the second commit reviewable, but I am happy to drop that commit if you
would rather it lived only in this PR description.

greggo74 and others added 2 commits September 6, 2026 12:31
Review of the connect/poll/reconnect paths for defects that cause or
worsen repeated loss of connection to the panel. 13 findings, ranked by
likely contribution, with file:line references against be1e46e.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…p stalls

Addresses the 13 findings in CONNECTION_STABILITY_REVIEW.md.

Tier 1:
- IO_TIMEOUT was bound as a default argument, evaluated at import time --
  before main() runs cfg.load() -- so every request path silently used the
  built-in 0.5 s and configuring it did nothing. Resolve it per call.
- Bound the status poll cycle with a panel-owned budget and floor the idle
  gap between cycles, so an overrunning cycle no longer re-polls a
  struggling panel back to back with zero delay.
- Reconnect backoff used '2 ^ retry' (XOR): 3, 0, 1, 6, 7, ... seconds, so
  the second attempt reconnected instantly and it never reached the cap.
- Failed IP connect attempts left their socket open and unowned; the retry
  overwrote _protocol and PAI competed with its own orphans for the
  module's single session slot. Close each attempt, and pause between them.

Tier 2 (STUN/paradoxmyhome):
- The TURN refresh ran inline in write(), doing blocking socket I/O on the
  event loop with no socket timeout. Run it in an executor, bound the
  sockets, and drop the link when it fails.
- Replaced a blocking time.sleep(5) in an async function, and bounded the
  SWAN site lookup.
- receive_response() assumed one recv() returned a whole STUN message.

Tier 3:
- busy.release() ran in a finally that could not have acquired the lock.
- gather left siblings running after the first status request failed.
- Connection.close() skipped its state reset when the protocol raised --
  which is the normal case when closing after a fault.
- An unparsable status block took the whole cycle down with it.
- disconnect() built a Connection just to ask whether one was open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 7, 2026

Copy link
Copy Markdown

@yozik04

yozik04 commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Did you try dev branch?

@greggo74

greggo74 commented Sep 7, 2026

Copy link
Copy Markdown
Author

Next on my list of todo. I'll do it today and let you know.
Tapped the PR option instead of branching option when Claude asked me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants