fix(connection): reconnect decision closes the failed socket (health_check's documented role) + symmetric backoff-bound test assertion (F3, F5)
This commit is contained in:
1 parent
24981d84a8
commit
9c409b3805
2 files changed
+27
-5
No files matched your search
@@ -276,7 +276,7 @@ class ShieldConnectionManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.connected and not await self._poller.connect():
|
if not self.connected and not await self._poller.connect():
|
||||||
self._note_failure(backoff)
|
await self._note_failure(backoff)
|
||||||
return self._jitter(backoff)
|
return self._jitter(backoff)
|
||||||
if self._failures:
|
if self._failures:
|
||||||
self._failures = 0
|
self._failures = 0
|
||||||
@@ -285,11 +285,11 @@ class ShieldConnectionManager:
|
|||||||
await self._refresh()
|
await self._refresh()
|
||||||
return ADB_POLL_INTERVAL
|
return ADB_POLL_INTERVAL
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
self._note_failure(backoff)
|
await self._note_failure(backoff)
|
||||||
_LOGGER.debug("Watchdog cycle failed: %s", err)
|
_LOGGER.debug("Watchdog cycle failed: %s", err)
|
||||||
return self._jitter(backoff)
|
return self._jitter(backoff)
|
||||||
|
|
||||||
def _note_failure(self, backoff: float) -> None:
|
async def _note_failure(self, backoff: float) -> None:
|
||||||
"""Bookkeep a failed cycle: log sparingly, drop stale store, notify."""
|
"""Bookkeep a failed cycle: log sparingly, drop stale store, notify."""
|
||||||
self._failures += 1
|
self._failures += 1
|
||||||
if self._failures == 1 or self._failures % 6 == 0:
|
if self._failures == 1 or self._failures % 6 == 0:
|
||||||
@@ -300,6 +300,13 @@ class ShieldConnectionManager:
|
|||||||
)
|
)
|
||||||
if not self._down_since:
|
if not self._down_since:
|
||||||
self._down_since = True
|
self._down_since = True
|
||||||
|
# Reconnect decision (health_check's documented role): close the
|
||||||
|
# socket so the next cycle rebuilds it from scratch instead of
|
||||||
|
# trusting shell()'s auto-reconnect on a broken transport.
|
||||||
|
try:
|
||||||
|
await self._poller.disconnect()
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.debug("disconnect during reconnect decision failed", exc_info=True)
|
||||||
self.store.clear(*ALL_FIELDS)
|
self.store.clear(*ALL_FIELDS)
|
||||||
self._notify()
|
self._notify()
|
||||||
|
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ class TestReconnectBackoff:
|
|||||||
# 5→10→20→40→80→160→300 growth with easing; the last must not exceed
|
# 5→10→20→40→80→160→300 growth with easing; the last must not exceed
|
||||||
# the cap (with jitter).
|
# the cap (with jitter).
|
||||||
assert max(delays) <= 300.0 * 1.25
|
assert max(delays) <= 300.0 * 1.25
|
||||||
assert delays[1] > delays[0] * 1.5
|
assert delays[1] >= delays[0] * 1.5
|
||||||
assert delays[-1] < 300.0 * 1.25
|
assert delays[-1] <= 300.0 * 1.25
|
||||||
|
|
||||||
async def test_repeated_failures_do_not_spam_listeners(self):
|
async def test_repeated_failures_do_not_spam_listeners(self):
|
||||||
poller = FakePoller()
|
poller = FakePoller()
|
||||||
@@ -245,6 +245,21 @@ class TestReconnectBackoff:
|
|||||||
assert manager._failures == 0
|
assert manager._failures == 0
|
||||||
assert await manager._watchdog_cycle(5.0) == ADB_POLL_INTERVAL
|
assert await manager._watchdog_cycle(5.0) == ADB_POLL_INTERVAL
|
||||||
|
|
||||||
|
async def test_failed_wake_poll_forces_reconnect(self):
|
||||||
|
"""health_check drives the reconnect decision: a cycle where is_awake
|
||||||
|
fails closes the socket so the next cycle reconnects from scratch."""
|
||||||
|
poller = FakePoller()
|
||||||
|
poller.connected = True
|
||||||
|
poller.awake = None # wake poll fails while the flag says "connected"
|
||||||
|
manager = _manager(poller)
|
||||||
|
|
||||||
|
delay = await manager._watchdog_cycle(5.0)
|
||||||
|
|
||||||
|
assert 5.0 * 0.75 <= delay <= 5.0 * 1.25 # F5-style symmetric bound
|
||||||
|
assert poller.disconnect_calls == 1
|
||||||
|
assert poller.connected is False
|
||||||
|
assert manager._failures == 1
|
||||||
|
|
||||||
async def test_connect_failure_clears_stale_store_state(self):
|
async def test_connect_failure_clears_stale_store_state(self):
|
||||||
poller = FakePoller()
|
poller = FakePoller()
|
||||||
poller.connect_failures = 1
|
poller.connect_failures = 1
|
||||||
|
|||||||
Reference in new issue
Block a user