Tolerate a restarting server after the native update
The first ping after `herdr update --handoff` was sent exactly once. A server still binding its socket turned into "更新/热切换未完成" even though the update had succeeded. Retry connection errors for up to 20 seconds before giving up; a healthy server still answers immediately. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+24
-1
@@ -32,6 +32,29 @@ def request(socket_path: str, method: str, params: dict, timeout: float = 10) ->
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def ping_when_reachable(socket_path: str, *, timeout: float = 20) -> dict:
|
||||||
|
"""Ping a server that may still be restarting right after an update.
|
||||||
|
|
||||||
|
The native updater can return while the new server is still binding the
|
||||||
|
socket; a single refused connection must not be reported as a failed
|
||||||
|
update. The first attempt is immediate so a healthy server adds no delay.
|
||||||
|
"""
|
||||||
|
deadline: float | None = None
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return request(socket_path, "ping", {}, timeout=2)
|
||||||
|
except (OSError, ValueError, RuntimeError) as error:
|
||||||
|
now = time.monotonic()
|
||||||
|
if deadline is None:
|
||||||
|
deadline = now + timeout
|
||||||
|
elif now >= deadline:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"更新后 {timeout:g} 秒内无法连接 Herdr 服务器:{error};"
|
||||||
|
"请检查 Session 状态,不会自动重启"
|
||||||
|
) from error
|
||||||
|
time.sleep(.2)
|
||||||
|
|
||||||
|
|
||||||
def installed_version(binary: str) -> str:
|
def installed_version(binary: str) -> str:
|
||||||
version = subprocess.check_output([binary, "--version"], text=True, timeout=10).strip()
|
version = subprocess.check_output([binary, "--version"], text=True, timeout=10).strip()
|
||||||
if not version.startswith("herdr ") or not version[6:].strip():
|
if not version.startswith("herdr ") or not version[6:].strip():
|
||||||
@@ -62,7 +85,7 @@ def update_and_handoff(binary: str, session: str) -> None:
|
|||||||
# Let the native updater perform its opt-in handoff, scoped to this session.
|
# Let the native updater perform its opt-in handoff, scoped to this session.
|
||||||
subprocess.run([binary, "--session", session, "update", "--handoff"], check=True, timeout=300)
|
subprocess.run([binary, "--session", session, "update", "--handoff"], check=True, timeout=300)
|
||||||
version = installed_version(binary)
|
version = installed_version(binary)
|
||||||
current = request(socket_path, "ping", {})
|
current = ping_when_reachable(socket_path)
|
||||||
if current.get("version") != version:
|
if current.get("version") != version:
|
||||||
if binary_identity(binary) != identity_before:
|
if binary_identity(binary) != identity_before:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
|||||||
@@ -65,6 +65,33 @@ class HandoffTests(unittest.TestCase):
|
|||||||
update_and_handoff("/fake/herdr", "named")
|
update_and_handoff("/fake/herdr", "named")
|
||||||
self.assertEqual([c.args[1] for c in call.call_args_list].count("server.live_handoff"), 1)
|
self.assertEqual([c.args[1] for c in call.call_args_list].count("server.live_handoff"), 1)
|
||||||
|
|
||||||
|
def test_server_restarting_after_native_update_is_polled_not_failed(self):
|
||||||
|
old = {"version": "0.9.0", "capabilities": {"live_handoff": True}}
|
||||||
|
with patch("herdr_hub.restart.subprocess.check_output", return_value="status: running\nsocket: /tmp/fake.sock\n"), patch(
|
||||||
|
"herdr_hub.restart.request",
|
||||||
|
side_effect=[old, ConnectionRefusedError(), FileNotFoundError(), {"version": "0.9.1"}],
|
||||||
|
) as call, patch("herdr_hub.restart.subprocess.run"), patch(
|
||||||
|
"herdr_hub.restart.installed_version", return_value="0.9.1"
|
||||||
|
), patch("herdr_hub.restart.time.sleep"), patch(
|
||||||
|
"herdr_hub.restart.time.monotonic", side_effect=[0, 0.2]
|
||||||
|
), contextlib.redirect_stdout(io.StringIO()):
|
||||||
|
update_and_handoff("/fake/herdr", "named")
|
||||||
|
self.assertEqual([c.args[1] for c in call.call_args_list], ["ping"] * 4)
|
||||||
|
|
||||||
|
def test_server_unreachable_after_update_is_reported_without_restart(self):
|
||||||
|
old = {"version": "0.9.0", "capabilities": {"live_handoff": True}}
|
||||||
|
with patch("herdr_hub.restart.subprocess.check_output", return_value="status: running\nsocket: /tmp/fake.sock\n"), patch(
|
||||||
|
"herdr_hub.restart.request",
|
||||||
|
side_effect=[old, ConnectionRefusedError(), ConnectionRefusedError()],
|
||||||
|
) as call, patch("herdr_hub.restart.subprocess.run"), patch(
|
||||||
|
"herdr_hub.restart.installed_version", return_value="0.9.1"
|
||||||
|
), patch("herdr_hub.restart.time.sleep"), patch(
|
||||||
|
"herdr_hub.restart.time.monotonic", side_effect=[0, 25]
|
||||||
|
), contextlib.redirect_stdout(io.StringIO()):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "无法连接 Herdr 服务器"):
|
||||||
|
update_and_handoff("/fake/herdr", "named")
|
||||||
|
self.assertNotIn("server.live_handoff", [c.args[1] for c in call.call_args_list])
|
||||||
|
|
||||||
def test_real_helper_updates_old_server_even_when_binary_already_latest(self):
|
def test_real_helper_updates_old_server_even_when_binary_already_latest(self):
|
||||||
self._run_real_helper()
|
self._run_real_helper()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user