diff --git a/skills/kvb-org-queue/scripts/queue.py b/skills/kvb-org-queue/scripts/queue.py index cc26af5..ee61430 100755 --- a/skills/kvb-org-queue/scripts/queue.py +++ b/skills/kvb-org-queue/scripts/queue.py @@ -406,7 +406,10 @@ def evaluate(entries: list[dict], free_gb: float, floor: float, stage: str, memo holds it, so a lower entry never takes a higher entry's slot; one blocked by disk or budget reserves its need against the entries after it. A hotfix entry that passes every check but the host load holds back - every entry after it, so lighter jobs cannot keep the load up. An + every entry after it, so lighter jobs cannot keep the load up. While no + queue job is active, any other entry the host load alone keeps out + reserves nothing and holds nothing back: that load lies outside the + queue, so holding the queue frees none of it. An exclusive entry that only running jobs of other kinds keep out holds back every entry after it but light ones, so none overtakes it. A waiting entry whose holder has not polled for CLAIM_SECONDS reserves nothing and @@ -435,13 +438,17 @@ def evaluate(entries: list[dict], free_gb: float, floor: float, stage: str, memo if reason is not None and kind in exclusive: drain = entry["id"] if reason is None: - reason, used, available = resources(need, share, used, available, floor, budget, taken) - if reason is None and entry["hotfix"] and entry["id"] in loaded: - reason, held = loaded[entry["id"]], entry["id"] - elif reason is None: - taken += 1 - counts[kind] += 1 - busy.add(workdir) + reason, after, left = resources(need, share, used, available, floor, budget, taken) + if reason is None and entry["id"] in loaded and (entry["hotfix"] or not active): + reason = loaded[entry["id"]] + if entry["hotfix"]: + held = entry["id"] + else: + used, available = after, left + if reason is None: + taken += 1 + counts[kind] += 1 + busy.add(workdir) verdicts[entry["id"]] = reason for entry in waiting: if silent(entry, now): diff --git a/tests/test_queue.py b/tests/test_queue.py index 63f421c..79d005e 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -296,9 +296,29 @@ class TestEvaluate(unittest.TestCase): self.decide([hotfix, light], loaded={"j1": load}), {"j1": load, "j2": "waits behind hotfix j1, which the host load holds back"}, ) - self.assertEqual(self.decide([job(1), light], loaded={"j1": load}), {"j1": None, "j2": None}) + self.assertEqual(self.decide([job(1), light], loaded={"j1": load}), {"j1": load, "j2": None}) self.assertEqual(self.decide([hotfix, light]), {"j1": None, "j2": None}) + def test_with_no_job_active_a_head_the_host_load_alone_keeps_out_reserves_nothing_and_with_one_active_it_does(self): + load = "RAM 55 % used + 41 % still to come reach the limit 90 %" + head, twin = job(1, kind="quality-high"), job(2, kind="quality-high", task="work/b") + gate, light = job(3, task="work/b"), job(4, kind="test-nostack", task="work/c") + shares = {"j1": {"ram": 41, "cpu": 62}, "j2": {"ram": 41, "cpu": 62}, "j3": {"ram": 20, "cpu": 40}, "j4": {"ram": 6, "cpu": 40}} + rules = {"slot_limits": {"quality-high": 1, "test": 5, "test-nostack": 6}, "exclusive": {"quality-high", "test"}, "light": {"test-nostack"}} + loaded = {"j1": load, "j2": load} + self.assertEqual( + self.decide([head, twin, gate, light], loaded=loaded, footprints=shares, **rules), + {"j1": load, "j2": load, "j3": None, "j4": None}, + ) + running = job(5, kind="test-nostack", task="work/c", status="active") + self.assertEqual( + self.decide([running, head, twin, gate, light], loaded=loaded, footprints=shares | {"j5": {"ram": 6, "cpu": 11}}, **rules), + { + "j1": None, "j2": "all 1 quality-high slots taken", "j3": "test runs only beside test-nostack, but quality-high runs", + "j4": "CPU 73 % in use or reserved + 40 % > budget 100 %; reserved", + }, + ) + class TestHistory(unittest.TestCase): @classmethod