diff --git a/scripts/ci_macos_admission.py b/scripts/ci_macos_admission.py index 278ca3bc65..d13395e281 100644 --- a/scripts/ci_macos_admission.py +++ b/scripts/ci_macos_admission.py @@ -71,6 +71,10 @@ def admitted(jobs): def run_demand(run, jobs): """macOS runners a run holds or still needs.""" + # A run with no jobs that is pending waits behind another run of its + # concurrency group, which holds the runners for both. + if not jobs and run["status"] in ("pending", "waiting"): + return 0 active = sum(1 for job in jobs if is_macos(job) and job["status"] in ACTIVE) if macos_done(jobs): return active @@ -127,10 +131,10 @@ def measure(api, repo, workflow, run_id, now): """Total macOS runners held or needed, one line per run that holds any, and whether run_id was already let in.""" total, lines, here = 0, [], False - # A run is listed as queued whenever one of its jobs waits for a runner, so a - # queued run can hold runners too. A run can move between the two lists - # between the calls. - runs = {run["id"]: run for status in ("in_progress", "queued") + # A run is listed as queued whenever one of its jobs waits for a runner, and + # as pending whenever one waits in a concurrency group, so runs in any of these + # can hold runners. A run can move between the lists between the calls. + runs = {run["id"]: run for status in ("in_progress", "queued", "pending", "waiting") for run in list_runs(api, repo, workflow, status=status)} for run in runs.values(): jobs = api.get(f"repos/{repo}/actions/runs/{run['id']}/jobs", diff --git a/scripts/tests/test_ci_macos_admission.py b/scripts/tests/test_ci_macos_admission.py index ec70267b48..a427536fd7 100644 --- a/scripts/tests/test_ci_macos_admission.py +++ b/scripts/tests/test_ci_macos_admission.py @@ -139,6 +139,18 @@ class MeasureTest(unittest.TestCase): self.assertEqual(len(lines), 3) self.assertFalse(here) + def test_counts_a_pending_run_that_holds_runners(self): + # One of its jobs waits in a concurrency group while its macOS builds run. + pending_pr = dict(PR, status="pending") + api = FakeApi({("runs", "pending"): [pending_pr], jobs_path(2): [gate("arm64"), build("arm64")]}) + total, _, _ = admission.measure(api, "o/r", "build_all.yml", 99, NOON) + self.assertEqual(total, admission.RESERVE) + + def test_a_push_waiting_behind_another_holds_nothing(self): + api = FakeApi({("runs", "pending"): [dict(PUSH, status="pending")], jobs_path(1): []}) + total, _, _ = admission.measure(api, "o/r", "build_all.yml", 99, NOON) + self.assertEqual(total, 0) + def test_reads_every_page_of_runs(self): runs = [dict(PR, id=i) for i in range(150)] responses = {("runs", "in_progress"): runs}