Preserve server status when batch probes miss the deadline

This commit is contained in:
2026-10-05 16:17:29 -04:00
parent ad63b3957d
commit ae703617af
7 changed files with 105 additions and 22 deletions
+40 -2
View File
@@ -613,7 +613,7 @@ class AgentTests(unittest.TestCase):
lock = threading.Lock()
active, peak = 0, 0
def rcon(instance_id, generation, command, deadline):
def rcon(instance_id, generation, command, deadline, status_attempts):
nonlocal active, peak
with lock:
active += 1
@@ -622,6 +622,7 @@ class AgentTests(unittest.TestCase):
ready.set()
try:
self.assertEqual(command, "status")
self.assertEqual(status_attempts, 1)
self.assertTrue(release.wait(2))
if instance_id == "00000000-0000-4000-8000-000000000002":
raise AgentError("local failure")
@@ -647,6 +648,43 @@ class AgentTests(unittest.TestCase):
self.assertEqual(results[0]["results"][2]["reply"]["reason"], "unreachable")
self.assertTrue(results[0]["results"][4]["reply"]["ok"])
def test_batch_uses_one_attempt_but_individual_status_still_retries(self):
saved = {"rconPassword": "secret", "port": 28960}
timeout = {"ok": False, "reason": "timeout", "text": ""}
operation = {"kind": "rcon.status-batch", "payload": {"instances": [{"instanceId": INSTANCE, "generation": 1}]}}
with patch.object(self.agent, "spec", return_value=saved), patch.object(self.agent, "rcon_attempt", return_value=timeout) as attempt, patch.object(module.time, "sleep") as sleep:
self.assertEqual(self.agent.dispatch(operation)["results"][0]["reply"], timeout)
attempt.assert_called_once()
sleep.assert_not_called()
attempt.reset_mock()
self.assertEqual(self.agent.rcon(INSTANCE, 1, "status"), timeout)
self.assertEqual(attempt.call_count, 3)
self.assertEqual(sleep.call_count, 2)
attempt.reset_mock()
self.assertEqual(self.agent.rcon(INSTANCE, 1, "status", deadline=0, status_attempts=1)["reason"], "deadline")
attempt.assert_not_called()
def test_batch_reaches_healthy_servers_after_four_unresponsive_servers(self):
clock = [0]
checked = []
targets = [{"instanceId": f"00000000-0000-4000-8000-{i:012d}", "generation": 1} for i in range(16)]
def attempt(family, host, port, password, command, deadline):
checked.append(port)
if port < 28964:
clock[0] += 3
return {"ok": False, "reason": "timeout", "text": ""}
return {"ok": True, "text": "status"}
# Serial execution is a conservative budget test; concurrency is tested separately.
with patch.object(module, "ThreadPoolExecutor") as pool, patch.object(module.time, "monotonic", side_effect=lambda: clock[0]), patch.object(self.agent, "spec", side_effect=lambda instance, generation: {"port": 28960 + int(instance[-12:]), "rconPassword": "secret"}), patch.object(self.agent, "rcon_attempt", side_effect=attempt), patch.object(module.time, "sleep") as sleep:
pool.return_value.__enter__.return_value.map.side_effect = map
result = self.agent.rcon_status_batch({"payload": {"instances": targets}})
self.assertEqual(len(checked), 16)
self.assertEqual(sum(row["reply"]["ok"] for row in result["results"]), 12)
self.assertEqual(clock[0], 12)
sleep.assert_not_called()
def test_status_batch_rejects_old_generations_and_bounds_expired_work(self):
marker = self.agent.instance_dir(INSTANCE, 1).parent / "current.json"
module.atomic_json(marker, {"generation": 2})
@@ -658,7 +696,7 @@ class AgentTests(unittest.TestCase):
marker.unlink()
with patch.object(module.time, "monotonic", side_effect=[0] + [21] * 20):
result = self.agent.dispatch(operation)
self.assertEqual(result["results"][0]["reply"]["reason"], "timeout")
self.assertEqual(result["results"][0]["reply"]["reason"], "deadline")
probe.assert_not_called()
for targets in ([], [None], [{"instanceId": INSTANCE, "generation": 1}] * 2):
with self.assertRaises(AgentError):