Skip to content

fix(aio): close open TCP connections in async_stop (stop hangs until the panel sends) - #92

Open
qaigor wants to merge 1 commit into
eavanvalkenburg:masterfrom
qaigor:fix/aio-tcp-stop-open-connections
Open

qaigor wants to merge 1 commit into
eavanvalkenburg:masterfrom
qaigor:fix/aio-tcp-stop-open-connections

Conversation

@qaigor

@qaigor qaigor commented Oct 27, 2026

Copy link
Copy Markdown

Summary

SIAClientTCP.async_stop() calls server.close() and then await server.wait_closed(). Since Python 3.12, wait_closed() also waits for every accepted connection to finish (docs, "Changed in version 3.12"). handle_line only checks shutdown_flag between reads and otherwise blocks in reader.read(1000), so async_stop() returns only after every connected panel has sent its next message, and never if a connection has gone silent.

In Home Assistant this shows up as a slow shutdown: the sia integration calls async_stop() from its EVENT_HOMEASSISTANT_STOP listener. My Ajax hub keeps two connections open (Ethernet and Wi-Fi) and sends a heartbeat on each every 60 s, so every HA stop waited up to a minute on SIA, and several times all three shutdown stages timed out (Timed out waiting for integrations to stop … SIAHub.async_shutdown()) and the stop took about 3.5 minutes.

This PR keeps the writers of open TCP connections in SIAServerTCP and closes them in async_stop() before wait_closed(). Closing the transport makes the pending reader.read() return, so handle_line leaves through its existing finally. It works on every supported Python; Server.close_clients() would do the same but only exists from 3.13.

Test plan

  • New test test_aio_tcp_stop_with_idle_connection: opens a connection that sends nothing and expects async_stop() to return within 2 s. Fails on master with TimeoutError, passes with the fix.
  • Full suite with the fix: 191 passed, 13 skipped on Python 3.12.3 and 3.14.6 (190 passed, 13 skipped on master).
  • pydocstyle src/pysiaalarm/aio/ clean.

Note: with the current unpinned test extras (pytest 9.1.1, pytest-cases 3.10.1) the suite doesn't start (TypeError: IdMaker.__init__() takes 7 positional arguments but 8 were given in pytest_cases). I ran it with pytest<9.

Notes for the reviewer

Validated on a live Home Assistant 2026.9.3 install (pysiaalarm 3.2.2, Python 3.14) with an Ajax Hub 2 Plus: with the same change applied, reloading the SIA config entry, which goes through the same async_stop(), took 0.03 s, and the hub reconnected on both channels within 4 s. Before, it waited for the next heartbeat. I also checked stopping while the event callback is still running and with a peer that never reads: async_stop() returns at once in both cases, and the callback finishes on its own.

Since Python 3.12, asyncio.Server.wait_closed() waits for every accepted
connection to finish, not only for the listening socket. handle_line
checks shutdown_flag only between reads and otherwise blocks in
reader.read(), so async_stop() returned only after every connected panel
had sent its next message, and never if a connection had gone silent.

The TCP server now keeps the writers of its open connections, and
async_stop() closes them before awaiting wait_closed().
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.

1 participant