mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +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
|
||||
`finally`) unregisters it with the frame recorder. New
|
||||
`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
|
||||
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
|
||||
|
||||
+6
-1
@@ -396,9 +396,9 @@ class StateSubscription:
|
||||
def _run(self) -> None:
|
||||
backoff = _RECONNECT_MIN_SECONDS
|
||||
while not self._stop.is_set():
|
||||
snapshots = self.snapshots
|
||||
try:
|
||||
self._follow()
|
||||
backoff = _RECONNECT_MIN_SECONDS
|
||||
except ControlError as e:
|
||||
self.last_error = e.reason
|
||||
if e.reason in _SLOW_RETRY_REASONS:
|
||||
@@ -414,6 +414,11 @@ class StateSubscription:
|
||||
sock.close()
|
||||
except OSError:
|
||||
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):
|
||||
return
|
||||
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
|
||||
|
||||
|
||||
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 ------------------------------------------------------------------
|
||||
|
||||
def _wait_until(predicate, timeout=5.0):
|
||||
|
||||
Reference in New Issue
Block a user