Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,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
Expand Down
7 changes: 6 additions & 1 deletion src/ipc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,9 @@ def stop(self, timeout: float = 2.0) -> None:
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:
Expand All @@ -391,6 +391,11 @@ def _run(self) -> None:
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)
Expand Down
46 changes: 46 additions & 0 deletions test/test_ipc_state_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,52 @@ def test_loop_age_counts_the_time_since_it_arrived(self, hub):
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):
Expand Down
Loading