fix(buzz): a clean relay close backs off and publishes retrying like any other disconnect
A relay that accepted, authenticated and subscribed and then closed cleanly made the read loop return without raising, so _websocket_loop reconnected immediately with no backoff and never flipped health to "retrying". The read loop now raises ConnectionError on StopAsyncIteration so the clean close takes the same backoff + degraded path as an idle or send-side disconnect.
This commit is contained in:
@@ -1138,7 +1138,7 @@ class BuzzAdapter(BasePlatformAdapter):
|
||||
backoff = min(backoff * 2, 30.0)
|
||||
|
||||
async def _ws_read_loop(self, websocket, subscriptions: Dict[str, Optional[str]]) -> None:
|
||||
"""Read frames until the relay closes; an idle read raises ConnectionError to reconnect."""
|
||||
"""Read frames until the relay closes; a close or an idle read raises ConnectionError to reconnect."""
|
||||
frame_iter = websocket.__aiter__()
|
||||
while True:
|
||||
read_task = asyncio.ensure_future(frame_iter.__anext__())
|
||||
@@ -1155,7 +1155,9 @@ class BuzzAdapter(BasePlatformAdapter):
|
||||
)
|
||||
raw = read_task.result()
|
||||
except StopAsyncIteration:
|
||||
return
|
||||
# A clean relay close is still a disconnect: raising sends it through the same
|
||||
# backoff + "retrying" path instead of reconnecting in a hot loop.
|
||||
raise ConnectionError("relay closed the WebSocket") from None
|
||||
finally:
|
||||
if not read_task.done():
|
||||
read_task.cancel()
|
||||
|
||||
@@ -254,6 +254,46 @@ async def test_websocket_loop_reconnects_when_discovery_send_sees_closed_socket(
|
||||
assert sockets[0].exited, "the dead connection was not closed before reconnecting"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_loop_backs_off_and_publishes_retrying_on_clean_relay_close(monkeypatch):
|
||||
"""A relay that accepts, then cleanly closes after subscribe, is a disconnect like any other.
|
||||
|
||||
StopAsyncIteration used to return from the read loop without raising, so the
|
||||
loop reconnected in a hot loop with no backoff and health stayed "connected".
|
||||
"""
|
||||
adapter = _make_adapter()
|
||||
states = []
|
||||
monkeypatch.setattr(adapter, "_write_runtime_status_safe", lambda status, **kw: states.append(kw["platform_state"]))
|
||||
|
||||
async def clean_close():
|
||||
raise StopAsyncIteration
|
||||
|
||||
sockets = []
|
||||
|
||||
def fake_connect(*args, **kwargs):
|
||||
ws = _ScriptedWebSocket(clean_close)
|
||||
sockets.append(ws)
|
||||
return ws
|
||||
|
||||
import websockets as _ws_mod
|
||||
|
||||
monkeypatch.setattr(_ws_mod, "connect", fake_connect)
|
||||
|
||||
task = asyncio.create_task(adapter._websocket_loop())
|
||||
try:
|
||||
await asyncio.sleep(0.3)
|
||||
finally:
|
||||
task.cancel()
|
||||
try:
|
||||
await asyncio.wait_for(task, 5.0)
|
||||
except (asyncio.CancelledError, asyncio.TimeoutError):
|
||||
pass
|
||||
|
||||
assert len(sockets) == 1, f"clean close must back off before reconnecting, got {len(sockets)} connects in 0.3s"
|
||||
assert sockets[0].exited, "the closed connection was not exited before backing off"
|
||||
assert states == ["retrying"], f"a clean relay close must publish retrying, got {states}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_loop_dispatches_frames_and_closes_cleanly(monkeypatch):
|
||||
"""The watchdog refactor preserves the healthy path: frames dispatch to
|
||||
|
||||
Reference in New Issue
Block a user