Report stalled agent lanes and bound health lock waits

This commit is contained in:
2026-10-06 12:07:34 -04:00
parent 532a10a880
commit 37d380fa45
8 changed files with 263 additions and 19 deletions
+61
View File
@@ -446,6 +446,67 @@ class AgentTests(unittest.TestCase):
self.assertEqual(logs.getvalue().count("RuntimeError"), 2)
self.assertNotIn("secret", logs.getvalue())
def test_lane_inventory_tracks_each_health_worker_and_detects_dead_threads(self):
def poll():
self.agent.lane_started("health-1")
self.agent.lane_polled("health-1")
with patch.object(module.time, "monotonic", return_value=100):
thread = threading.Thread(target=poll)
thread.start()
thread.join(timeout=2)
self.assertFalse(thread.is_alive())
with patch.object(module.time, "monotonic", return_value=150):
self.agent.lane_started("health-2")
self.agent.lane_polled("health-2")
self.agent.lane_busy("health-2", True)
with patch.object(module.time, "monotonic", return_value=200):
lanes = {lane["name"]: lane for lane in self.agent.lane_inventory()}
self.assertEqual(set(lanes), {"health-1", "health-2", "read", "write"})
self.assertFalse(lanes["health-1"]["alive"])
self.assertEqual(lanes["health-1"]["pollAgeSeconds"], 100)
self.assertTrue(lanes["health-2"]["alive"])
self.assertTrue(lanes["health-2"]["busy"])
self.assertEqual(lanes["health-2"]["pollAgeSeconds"], 50)
self.assertIsNotNone(lanes["health-2"]["lastPollAt"])
self.assertEqual(len(self.agent.inventory()["agentLanes"]), 4)
def test_health_lane_times_out_on_instance_lock_and_processes_next_check(self):
blocked = self.operation("instance.inspect", op_id="blocked-health")
fresh = {**self.operation("instance.inspect", op_id="fresh-health"), "instanceId": "other-instance"}
pending = iter([blocked, fresh])
failures = []
def request(method, path, payload=None):
if method == "POST":
return {"operation": {"id": path.rsplit("/", 1)[1], "state": payload["state"]}}
operation = next(pending, None)
if operation is None:
self.agent.stopping.set()
return {"operations": [operation] if operation else []}
def read():
try:
self.agent.read_loop("health")
except BaseException as error:
failures.append(error)
with patch.object(module, "HEALTH_LOCK_TIMEOUT_SECONDS", 0.01), patch.object(self.agent, "request", side_effect=request), patch.object(self.agent, "dispatch", return_value={"healthy": True}) as dispatch:
with self.agent.instance_lock(blocked):
thread = threading.Thread(target=read)
thread.start()
thread.join(timeout=2)
finished_while_locked = not thread.is_alive()
self.agent.stopping.set()
thread.join(timeout=2)
self.assertTrue(finished_while_locked)
self.assertEqual(failures, [])
self.assertEqual(self.agent.journal.get(blocked["id"])["state"], "failed")
self.assertIn("waiting for the instance", self.agent.journal.get(blocked["id"])["error"])
self.assertEqual(self.agent.journal.get(fresh["id"])["state"], "completed")
dispatch.assert_called_once_with(fresh)
self.assertEqual(self.agent.read_claims, set())
def test_inventory_reuses_manifests_until_mtime_changes(self):
marker = self.library / "manifest.json"
saved = json.loads(marker.read_text())