Commit 1877cd0
authored
fix: keep the WebSocket pool usable after an event-loop change (#2001)
* fix: detach the WebSocket pool before cleaning it up on an event-loop change
Pooled clients belong to the loop they were created on. When the loop
changed, get_client awaited disconnect() for every stale client from the
new loop, which raises RuntimeError ("attached to a different loop").
That class was not in the best-effort catch, so it escaped before
_clients.clear() and before the _current_loop update: the pool stayed
stale, every later call re-entered the same branch, and WebSocket-backed
tools kept failing until the process restarted.
The pool and the loop reference are now detached first, and cleanup runs
without awaiting anything across loops: a still-running owning loop gets
each disconnect scheduled on it, a closed or stopped one leaves nothing
to schedule so the connection is abandoned with the loop's resources.
WebSocketManager.disconnect gets the same detach-first order for the
shutdown path, and both remaining best-effort catches around
client.disconnect() now include RuntimeError.
* fix: report scheduled stale-disconnect outcomes and pin the detach order
Review follow-ups on the loop-change fix:
- A disconnect scheduled on a still-running owning loop was fire-and-forget
into a discarded concurrent.futures.Future, which never warns about an
exception nobody retrieved, so a failure there was invisible at every log
level. A done-callback now reports cancellation and failure at debug level.
- The docstring claimed an abandoned connection's socket is released with the
loop's resources. Closing a loop does not close its transports; the socket
goes when the orphaned transport is garbage-collected, with a
ResourceWarning. Reworded, and the residual scheduling window (a loop that
accepts the callback and then stops before draining it) is now named
instead of implied to be closed.
- The disconnect() regression test asserted only the final pool state, which
the widened except alone satisfies, so the detach-before-cleanup order was
untested. The stub now captures the pool from inside its disconnect call and
the test asserts it is already empty.
* fix: release pooled clients when the old loop is only stopped
A loop that is stopped but not closed still owns its transports and can
be resumed with run_forever()/run_until_complete(), so treating it like a
closed loop dropped the manager's only reference to those clients without
scheduling their disconnect: the WebSocket and its background task stayed
rooted by the retained loop while a replacement connection was opened, and
repeated loop swaps leaked one connection each.
run_coroutine_threadsafe accepts a stopped, non-closed loop; the callback
is queued and runs when the loop is resumed. Narrow the abandon condition
to loops that are closed or unknown, which are the only ones with nothing
left to schedule on.
* test: pin the proxy branch in the journald window lines cases
The two proxy cases in TestJournaldWindowLinesParam asserted on the
HA-Core proxy path by mocking mock_client.httpx_client.request, but did
not declare which branch get_addon_logs / _get_system_service_logs should
take. Both branch on is_running_in_addon(), which keys off SUPERVISOR_TOKEN
in the environment, so with that variable set the call went down the
Supervisor-direct path instead, never touched the mocked attribute, and
issued a real request to http://supervisor. CI never sets the variable, so
the failure only appears when the unit suite runs inside an add-on
container.
Take the non_addon_install fixture, which the sibling cases in this file
already use, so the branch is an explicit input on every machine (#2000).
* test: cover the stale-disconnect callback and schedule-race branches
Pin the previously untested paths of the fire-and-forget cleanup added
for the loop-change fix:
- _log_stale_disconnect on a cancelled future and on a failed one. The
cancelled() guard has to precede exception(), since calling exception()
on a cancelled concurrent.futures.Future raises CancelledError; without
the guard the callback itself would raise.
- _release_stale_clients when run_coroutine_threadsafe raises RuntimeError
(the owning loop closing between the is_closed() check and the schedule
call). Two clients pin the continue, and the orphaned coroutines are
closed rather than left "never awaited".
Also correct the disconnect() comment, which claimed parity with
get_client()'s loop-change branch it does not have: the sole caller runs
on the pool's own loop and awaits each disconnect, and the RuntimeError
catch there is purely defensive.1 parent 363132b commit 1877cd0
3 files changed
Lines changed: 405 additions & 20 deletions
File tree
- src/ha_mcp/client
- tests/src/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| 11 | + | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
| |||
1036 | 1037 | | |
1037 | 1038 | | |
1038 | 1039 | | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
1039 | 1055 | | |
1040 | 1056 | | |
1041 | 1057 | | |
| |||
1114 | 1130 | | |
1115 | 1131 | | |
1116 | 1132 | | |
| 1133 | + | |
| 1134 | + | |
| 1135 | + | |
| 1136 | + | |
| 1137 | + | |
| 1138 | + | |
| 1139 | + | |
| 1140 | + | |
| 1141 | + | |
| 1142 | + | |
| 1143 | + | |
| 1144 | + | |
| 1145 | + | |
| 1146 | + | |
| 1147 | + | |
| 1148 | + | |
| 1149 | + | |
| 1150 | + | |
| 1151 | + | |
| 1152 | + | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
| 1181 | + | |
| 1182 | + | |
1117 | 1183 | | |
1118 | 1184 | | |
1119 | 1185 | | |
| |||
1143 | 1209 | | |
1144 | 1210 | | |
1145 | 1211 | | |
1146 | | - | |
1147 | | - | |
1148 | | - | |
1149 | | - | |
1150 | | - | |
1151 | | - | |
1152 | | - | |
1153 | | - | |
1154 | | - | |
1155 | | - | |
1156 | | - | |
1157 | | - | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
| 1218 | + | |
| 1219 | + | |
| 1220 | + | |
| 1221 | + | |
1158 | 1222 | | |
1159 | 1223 | | |
1160 | 1224 | | |
1161 | 1225 | | |
| 1226 | + | |
1162 | 1227 | | |
1163 | 1228 | | |
1164 | 1229 | | |
| |||
1226 | 1291 | | |
1227 | 1292 | | |
1228 | 1293 | | |
1229 | | - | |
| 1294 | + | |
1230 | 1295 | | |
1231 | 1296 | | |
1232 | 1297 | | |
| |||
1239 | 1304 | | |
1240 | 1305 | | |
1241 | 1306 | | |
1242 | | - | |
| 1307 | + | |
| 1308 | + | |
| 1309 | + | |
| 1310 | + | |
| 1311 | + | |
| 1312 | + | |
| 1313 | + | |
| 1314 | + | |
| 1315 | + | |
| 1316 | + | |
| 1317 | + | |
| 1318 | + | |
| 1319 | + | |
| 1320 | + | |
| 1321 | + | |
1243 | 1322 | | |
1244 | 1323 | | |
1245 | | - | |
| 1324 | + | |
1246 | 1325 | | |
1247 | 1326 | | |
1248 | 1327 | | |
1249 | | - | |
1250 | | - | |
1251 | | - | |
1252 | 1328 | | |
1253 | 1329 | | |
1254 | 1330 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1502 | 1502 | | |
1503 | 1503 | | |
1504 | 1504 | | |
1505 | | - | |
| 1505 | + | |
| 1506 | + | |
| 1507 | + | |
1506 | 1508 | | |
1507 | 1509 | | |
1508 | 1510 | | |
| |||
1514 | 1516 | | |
1515 | 1517 | | |
1516 | 1518 | | |
1517 | | - | |
| 1519 | + | |
| 1520 | + | |
| 1521 | + | |
1518 | 1522 | | |
1519 | 1523 | | |
1520 | 1524 | | |
| |||
0 commit comments