mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 14:55:08 +00:00
fix(ipc): reset the state subscription's reconnect wait after a good connection (#740)
StateSubscription._run reset its backoff only when _follow() returned normally, which happens only on stop(). Every real disconnect raises ControlError, so the wait kept doubling across connections: after successive display restarts the web resubscribed 1, 2, 4, 8, 16 and then 30 s later for good, answering from one-shot state.get connections in the meantime. The docs promise "1 s up to 30 s" per outage. The wait now goes back to the minimum once a connection got as far as storing a snapshot, whatever ended it. A display without the stream (unknown_command) is still retried at the slow interval. The frozen-timestamp bug found in the same review is fixed by #737. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -515,6 +515,16 @@ policies are unchanged.
|
|||||||
`DisplayManager.cleanup()` (reached from SIGTERM through `run()`'s
|
`DisplayManager.cleanup()` (reached from SIGTERM through `run()`'s
|
||||||
`finally`) unregisters it with the frame recorder. New
|
`finally`) unregisters it with the frame recorder. New
|
||||||
`frame_timing.uninstall_gc_monitor()`.
|
`frame_timing.uninstall_gc_monitor()`.
|
||||||
|
- The web interface's state subscription (`StateSubscription`,
|
||||||
|
`src/ipc/client.py`) resubscribes about 1 s after a display restart, every
|
||||||
|
time. Its reconnect wait went back to the minimum only when the
|
||||||
|
subscription was stopped. A disconnect after a working connection kept
|
||||||
|
doubling the wait, so successive display restarts were followed by waits
|
||||||
|
of 1, 2, 4, 8, 16 and then 30 s for good.
|
||||||
|
During each wait the web answered from one-shot `state.get` connections
|
||||||
|
instead of its copy. The wait now resets once a connection has stored a
|
||||||
|
snapshot. A display that does not offer the stream is still retried
|
||||||
|
slowly.
|
||||||
- A plugin reload after a store update (`plugin.reload`, #720) no longer
|
- A plugin reload after a store update (`plugin.reload`, #720) no longer
|
||||||
freezes the panel during Vegas. On ledpi a football reload froze it for
|
freezes the panel during Vegas. On ledpi a football reload froze it for
|
||||||
3.0 s (`Render stall over: no frame for 3043ms`). The reload ran on the
|
3.0 s (`Render stall over: no frame for 3043ms`). The reload ran on the
|
||||||
|
|||||||
+6
-1
@@ -396,9 +396,9 @@ class StateSubscription:
|
|||||||
def _run(self) -> None:
|
def _run(self) -> None:
|
||||||
backoff = _RECONNECT_MIN_SECONDS
|
backoff = _RECONNECT_MIN_SECONDS
|
||||||
while not self._stop.is_set():
|
while not self._stop.is_set():
|
||||||
|
snapshots = self.snapshots
|
||||||
try:
|
try:
|
||||||
self._follow()
|
self._follow()
|
||||||
backoff = _RECONNECT_MIN_SECONDS
|
|
||||||
except ControlError as e:
|
except ControlError as e:
|
||||||
self.last_error = e.reason
|
self.last_error = e.reason
|
||||||
if e.reason in _SLOW_RETRY_REASONS:
|
if e.reason in _SLOW_RETRY_REASONS:
|
||||||
@@ -414,6 +414,11 @@ class StateSubscription:
|
|||||||
sock.close()
|
sock.close()
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
if self.snapshots != snapshots:
|
||||||
|
# This connection got as far as the display's state: whatever
|
||||||
|
# ended it (a restart, most often), it was working, so the
|
||||||
|
# next try starts from the shortest wait again.
|
||||||
|
backoff = _RECONNECT_MIN_SECONDS
|
||||||
if self._stop.wait(backoff):
|
if self._stop.wait(backoff):
|
||||||
return
|
return
|
||||||
backoff = min(backoff * 2, _RECONNECT_MAX_SECONDS)
|
backoff = min(backoff * 2, _RECONNECT_MAX_SECONDS)
|
||||||
|
|||||||
@@ -346,6 +346,52 @@ class TestSubscriptionStore:
|
|||||||
assert client.snapshot_loop_age(snap, now_mono=104.0) is None
|
assert client.snapshot_loop_age(snap, now_mono=104.0) is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestReconnectBackoff:
|
||||||
|
"""StateSubscription._run's waits between connections, without a socket."""
|
||||||
|
|
||||||
|
def test_a_connection_that_got_a_snapshot_starts_the_backoff_over(self, hub,
|
||||||
|
monkeypatch):
|
||||||
|
"""Three failed tries, then the display is back twice, restarting
|
||||||
|
each time, then gone again. Each restart is retried after the
|
||||||
|
shortest wait, not after whatever the waits had grown to."""
|
||||||
|
sub = client.StateSubscription(paths=['/nowhere'])
|
||||||
|
script = ['refused', 'refused', 'refused', 'snapshot', 'snapshot', 'refused']
|
||||||
|
waits = []
|
||||||
|
|
||||||
|
def follow():
|
||||||
|
step = script.pop(0)
|
||||||
|
if step == 'snapshot': # subscribed, then the display restarted
|
||||||
|
sub._store(hub.snapshot(), full=True)
|
||||||
|
raise client.ControlError('closed', 'the display closed the connection')
|
||||||
|
raise client.ControlError(step)
|
||||||
|
|
||||||
|
def wait(seconds):
|
||||||
|
waits.append(seconds)
|
||||||
|
return not script # True ends _run, as stop() would
|
||||||
|
|
||||||
|
monkeypatch.setattr(sub, '_follow', follow)
|
||||||
|
monkeypatch.setattr(sub._stop, 'wait', wait)
|
||||||
|
sub._run()
|
||||||
|
first = client._RECONNECT_MIN_SECONDS
|
||||||
|
assert waits == [first, 2 * first, 4 * first, first, first, 2 * first]
|
||||||
|
|
||||||
|
def test_a_display_without_the_stream_is_still_retried_slowly(self, monkeypatch):
|
||||||
|
sub = client.StateSubscription(paths=['/nowhere'])
|
||||||
|
waits = []
|
||||||
|
|
||||||
|
def follow():
|
||||||
|
raise client.ControlError('unknown_command')
|
||||||
|
|
||||||
|
def wait(seconds):
|
||||||
|
waits.append(seconds)
|
||||||
|
return len(waits) == 2
|
||||||
|
|
||||||
|
monkeypatch.setattr(sub, '_follow', follow)
|
||||||
|
monkeypatch.setattr(sub._stop, 'wait', wait)
|
||||||
|
sub._run()
|
||||||
|
assert waits == [client._RECONNECT_MAX_SECONDS] * 2
|
||||||
|
|
||||||
|
|
||||||
# --- a real socket ------------------------------------------------------------------
|
# --- a real socket ------------------------------------------------------------------
|
||||||
|
|
||||||
def _wait_until(predicate, timeout=5.0):
|
def _wait_until(predicate, timeout=5.0):
|
||||||
|
|||||||
Reference in New Issue
Block a user