Repository navigation
ci: live runners decide root, gui and side routing; CI_OWNED_POOL_SLOTS is the fallback only #15572
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -534,8 +534,9 @@ def gui_runner(choice: "Choice", owned_slots: Mapping[str, int]) -> str: | |
| (guiRunners) carrying `glaeda-gui-<class>-xcode-<version>`, whose listener | ||
| stops while the gui token or every root is taken, so a GUI job waits in | ||
| GitHub's queue for a mini that can run it. Only on a pool with a root | ||
| count, and only while CI_OWNED_POOL_SLOTS gives the gui label a count, | ||
| so a GUI job never waits on a label no runner carries. | ||
| count, and only while an online runner carries the gui label (routing_slots(): the | ||
| variable only when the runners cannot be read), so a GUI job never waits on a label no | ||
| runner carries. | ||
| """ | ||
| if not choice.root_runner or not persistent(choice.runner): | ||
| return "" | ||
|
|
@@ -547,8 +548,9 @@ def side_runner(choice: "Choice", owned_slots: Mapping[str, int]) -> str: | |
| """The label a pick's side lanes take: the pool's side label, or "" to keep the pool label. | ||
|
|
||
| Only on a pool with a root count (the root and side runners are split), | ||
| and only while CI_OWNED_POOL_SLOTS leaves it machines beyond its root | ||
| runners, so a side lane never waits on a label no runner carries. | ||
| and only while the pool has machines beyond its root runners (routing_slots(): its | ||
| online runners, or CI_OWNED_POOL_SLOTS when they cannot be read), so a side lane never | ||
| waits on a label no runner carries. | ||
| """ | ||
| if not choice.root_runner or not persistent(choice.runner): | ||
| return "" | ||
|
|
@@ -562,9 +564,9 @@ def light_side_lanes(plan: "RunJobs", runners: Sequence[Mapping[str, Any]], owne | |
| """The light pool's side label and the side lanes of `plan` its idle side runners take now, one per runner. | ||
|
|
||
| release-build (RELEASE_BUILD_JOB), a universal Release compile, is never | ||
| one of them. ("", ()) when none is idle, and always while CI_OWNED_POOL_SLOTS gives | ||
| one of them. ("", ()) when none is idle, and always while `owned_slots` (routing_slots()) gives | ||
| the light pool no machines beyond its root runners (side_runner()'s | ||
| rule), so removing that count turns it off. | ||
| rule). | ||
| """ | ||
| light = next((label for label in owned_pools(pr_xcode_app) if label.startswith(f"glaeda-{LIGHT_CLASS}-")), "") | ||
| label = side_label(light) | ||
|
|
@@ -913,6 +915,24 @@ def slots(raw: str | None, pr_xcode_app: str | None = None) -> dict[str, int]: | |
| return _slots(raw, pr_xcode_app)[0] | ||
|
|
||
|
|
||
| def routing_slots(raw: str | None, pr_xcode_app: str | None, | ||
| runners: Sequence[Mapping[str, Any]] | None) -> dict[str, int]: | ||
| """The owned labels that route, with their machines: the online runners carrying each when the runners | ||
| were read (`runners`), else CI_OWNED_POOL_SLOTS (slots()). | ||
|
|
||
| The variable used to decide whether a pool routes its root jobs to the root label, its GUI jobs to the | ||
| gui label and its side lanes to the side label even when the runners API gave the live answer, so a | ||
| hand-set count could route jobs to a label no runner online carries, or keep them off one that every | ||
| mini carries. With the runners read, a label routes while one online runner carries it; the variable | ||
| is only the fallback when they cannot be read (the capacity counts in live_pools() already were). | ||
| """ | ||
| if runners is None: | ||
| return slots(raw, pr_xcode_app) | ||
| labels = [label for pool_name in owned_pools(pr_xcode_app) | ||
| for label in (pool_name, root_label(pool_name), gui_label(pool_name))] | ||
| return {label: count for label, count in live_online(runners, labels).items() if count > 0} | ||
|
|
||
|
|
||
| def capability_slots(raw: str | None) -> dict[str, int]: | ||
| """CI_OWNED_POOL_SLOTS: capability label -> machines carrying it (`{"glaeda-ios-sim": 2}`).""" | ||
| try: | ||
|
|
@@ -1335,8 +1355,9 @@ def live_pools(snapshot: Mapping[str, Any], idle: Mapping[str, int], slot_counts | |
| what holds the label now plus what choose() passes: the peaks of runs | ||
| younger than DEFAULT_JOB_MINUTES, less what they already hold. | ||
|
|
||
| A root label counts only while CI_OWNED_POOL_SLOTS gives it a root count, | ||
| which is what turns root routing on (root_label()). | ||
| A root label counts only while `slot_counts` (routing_slots(): the online | ||
| runners carrying it, or CI_OWNED_POOL_SLOTS when they cannot be read) has | ||
| it, which is what turns root routing on (root_label()). | ||
| """ | ||
| pools = dict(snapshot.get("pools") or {}) | ||
| capacity: dict[str, int] = {} | ||
|
|
@@ -2296,10 +2317,6 @@ def count_routed(since: str) -> int: | |
| # jobs at their peak. | ||
| gui = (env.get("POOL_OWNED_GUI") or "").strip() != "0" | ||
| jobs = owned_peak(plan, gui) | ||
| # The slots name gui runners (gui_runner()): the GUI jobs then hold no root runner. The pool is not | ||
| # picked yet, so any gui count counts here; place() below checks the picked pool's own. | ||
| gui_runners = any(label.startswith(GUI_PREFIX) | ||
| for label in slots(env.get("OWNED_SLOTS"), env.get(PR_XCODE_VARIABLE))) | ||
| # The org App's token (ci.yml mints it for same-repository pull requests | ||
| # only) reads which owned runners are idle now. Without it, or on any | ||
| # error, the slot counts and the snapshot decide as before. | ||
|
|
@@ -2317,12 +2334,18 @@ def count_routed(since: str) -> int: | |
| except Exception as error: # noqa: BLE001 - the snapshot path still decides | ||
| print(f"::warning title=live owned capacity::could not list runners ({error}); using the snapshot") | ||
| live_owned = online = live_runners = None | ||
| # Which owned labels route, and their machines: the online runners when they were read, the | ||
| # variable only when they could not be (routing_slots()). | ||
| routing = routing_slots(env.get("OWNED_SLOTS"), env.get(PR_XCODE_VARIABLE), live_runners) | ||
| routing_raw = env.get("OWNED_SLOTS") if live_runners is None else json.dumps(routing) | ||
| # Gui runners route (gui_runner()): the GUI jobs then hold no root runner. The pool is not | ||
| # picked yet, so any gui label counts here; place() below checks the picked pool's own. | ||
| gui_runners = any(label.startswith(GUI_PREFIX) for label in routing) | ||
| # As many side lanes as the light minis' side runners idle now (light_side_lanes()) take them: the pool | ||
| # picked below then holds admission, what follows it and the other side lanes. | ||
| light_side, side_lanes = "", () | ||
| if live_runners is not None and attempt in ("", "1") and event == "pull_request" and env.get("HEAD_REPO") == repo: | ||
| light_side, side_lanes = light_side_lanes( | ||
| plan, live_runners, slots(env.get("OWNED_SLOTS"), env.get(PR_XCODE_VARIABLE)), env.get(PR_XCODE_VARIABLE)) | ||
| light_side, side_lanes = light_side_lanes(plan, live_runners, routing, env.get(PR_XCODE_VARIABLE)) | ||
| if side_lanes: | ||
| plan = dataclasses.replace(plan, side=tuple(key for key in plan.side if key not in side_lanes)) | ||
| jobs = owned_peak(plan, gui) | ||
|
|
@@ -2341,7 +2364,7 @@ def count_routed(since: str) -> int: | |
| order=env.get("POOL_ORDER"), | ||
| max_queued=env.get("POOL_MAX_QUEUED"), | ||
| owned=env.get("POOL_OWNED"), | ||
| owned_slots=env.get("OWNED_SLOTS"), | ||
| owned_slots=routing_raw, | ||
| jobs=jobs, | ||
| split=env.get("POOL_OWNED_SPLIT"), | ||
| root_jobs=root_peak(plan, gui, gui_runners), | ||
|
|
@@ -2371,7 +2394,7 @@ def count_routed(since: str) -> int: | |
| print(f"::error title={SLOTS_VARIABLE}::{problem}") | ||
| # A persistent pick names the jobs that take it; every other job of the | ||
| # run takes retry_runner. The marker's jobs are the owned machines held. | ||
| owned_slots = slots(env.get("OWNED_SLOTS"), pr_xcode_app) | ||
| owned_slots = routing | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '545,575p;910,945p;2325,2415p' scripts/ci/pr_runner_pool.pyRepository: manaflow-ai/cmux Length of output: 9346 🏁 Script executed: set -e
printf '%s\n' '--- side_runner and nearby callers ---'
sed -n '500,590p' scripts/ci/pr_runner_pool.py
printf '%s\n' '--- side_runner references ---'
rg -n -C 4 'side_runner\(' scripts/ci/pr_runner_pool.py
printf '%s\n' '--- live runner acquisition and routing inputs ---'
rg -n -C 8 'live_runners|routing_slots|routing_raw|live_online|runners\(\)' scripts/ci/pr_runner_pool.pyRepository: manaflow-ai/cmux Length of output: 27094 🏁 Script executed: set -e
printf '%s\n' '--- side assignment and outputs ---'
sed -n '2438,2495p' scripts/ci/pr_runner_pool.py
printf '%s\n' '--- configured slot parsing and validation ---'
sed -n '850,925p;955,1000p' scripts/ci/pr_runner_pool.py
printf '%s\n' '--- tests and callers for routing_slots/side_runner ---'
rg -n -C 5 'routing_slots|side_runner|owned_slots.*side|SIDE_PREFIX' --glob '*.py' --glob '*.yml' --glob '*.yaml' .Repository: manaflow-ai/cmux Length of output: 42453 Gate side routing on a live side label without changing fallback behavior. When Apply the side-label check only to live routing. Keep the pool-minus-root calculation when the runner list is unavailable because Suggested fix-def side_runner(choice: "Choice", owned_slots: Mapping[str, int]) -> str:
+def side_runner(choice: "Choice", owned_slots: Mapping[str, int], live: bool = False) -> str:
...
+ label = side_label(choice.runner)
+ if live:
+ return label if owned_slots.get(label, 0) > 0 else ""
if owned_slots.get(choice.runner, 0) <= owned_slots.get(choice.root_runner, 0):
return ""
- return side_label(choice.runner)
+ return label
...
- for label in (pool_name, root_label(pool_name), gui_label(pool_name))]
+ for label in (pool_name, root_label(pool_name), side_label(pool_name), gui_label(pool_name))]
...
- side = side_runner(choice, owned_slots)
+ side = side_runner(choice, owned_slots, live_runners is not None)🤖 Prompt for AI Agents |
||
| gui_label_out = gui_runner(choice, owned_slots) | ||
| if choice.runner.startswith(f"glaeda-{LIGHT_CLASS}-"): | ||
| # The light pool's own pick places no universal Release compile; it keeps MACOS_RUNNER_26. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '785,815p;1380,1510p;2325,2410p' scripts/ci/pr_runner_pool.pyRepository: manaflow-ai/cmux
Length of output: 15598
🏁 Script executed:
Repository: manaflow-ai/cmux
Length of output: 17268
🏁 Script executed:
Repository: manaflow-ai/cmux
Length of output: 11884
Calculate root demand per candidate pool.
main()computes one globalroot_jobsvalue beforechoose()selects a pool.choose()andpick()pass and apply that scalar value to every owned candidate. Final placement uses the selected pool's own GUI label. A GUI runner in one pool can therefore reduce root demand for another pool and cause avoidable Blacksmith fallback.Calculate root demand per candidate, or recalculate it from the selected pool before accepting the choice. This is a localized change with meaningful routing benefit.
🤖 Prompt for AI Agents