fix(ci): let any strictly-higher PR reclaim a broken/draft run (#342)
Restore (and extend to drafts) the old bash's broken-reclaim behaviour that the
initial Python refactor had dropped. runs_to_cancel now cancels an OTHER PR's
active/queued runs when EITHER:
(a) THIS PR is P0 and that PR is strictly-lower (reclaim every lower runner); OR
(b) that PR is broken/draft (effective priority 10) and THIS PR is strictly-higher
(effective priority < 10) — a wasted run any ready PR may reclaim.
P1-P9 still never bump a *normal* (non-broken/draft) lower run; a broken/draft PR
(P10) preempts nothing (nothing is strictly-lower than the bottom, and the
equal-or-higher invariant means a P10 never cancels another P10). Self / main-push /
equal-or-higher invariants unchanged.
Updates the module docstring + ci.yml comments (the "only P0 preempts" wording
becomes: P0 preempts everything strictly-lower; additionally, any strictly-higher PR
preempts a broken/draft run) and the job step/permission/needs comments. Adds unit
tests: P3 reclaims a broken P10 run and a draft P10 run; P3 does not bump a normal P5
run; a P10 self preempts nothing; plus an end-to-end P5-reclaims-draft-then-waits
scenario. 37 unit tests pass; ci.yml parses clean; --dry-run shows a P3 cancelling a
draft (and broken) run while still yielding to a higher P1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,11 @@
|
||||
"""Unit tests for the pure decision core of traffic_control.py (no network).
|
||||
|
||||
Covers: priority resolution (P-label / broken / draft / default P5), PASS 1
|
||||
preemption (only P0 cancels strictly-lower; P1-P9 never bump; self / main /
|
||||
equal-or-higher never cancelled), PASS 2 hold-back (yield to strictly-higher with
|
||||
an active run; same-level running-first then oldest-first), and a few end-to-end
|
||||
decision scenarios."""
|
||||
preemption (P0 reclaims all strictly-lower; ANY higher PR reclaims a broken/draft
|
||||
lower run; P1-P9 never bump a *normal* lower run; self / main / equal-or-higher
|
||||
never cancelled), PASS 2 hold-back (yield to strictly-higher with an active run;
|
||||
same-level running-first then oldest-first), and a few end-to-end decision
|
||||
scenarios."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -63,17 +64,40 @@ class EffectivePriorityTests(unittest.TestCase):
|
||||
|
||||
|
||||
class RunsToCancelTests(unittest.TestCase):
|
||||
def test_non_p0_self_cancels_nothing(self):
|
||||
def test_non_p0_self_does_not_bump_normal_lower_run(self):
|
||||
# P1-P9 never preempt a *normal* strictly-lower run — they yield instead.
|
||||
me = pr(1, ["P1"])
|
||||
others = [pr(2, ["P5"], status=tc.RUNNING, run_ids=[200])]
|
||||
self.assertEqual(tc.runs_to_cancel(me, [me, *others]), [])
|
||||
|
||||
def test_non_p0_self_never_cancels_even_broken(self):
|
||||
# Behaviour change vs the old bash (see module/PR notes): P1-P9 no longer
|
||||
# reclaim a broken target's runner — only P0 preempts.
|
||||
def test_non_p0_self_reclaims_broken_lower_run(self):
|
||||
# Any higher-priority PR (not just P0) may reclaim a broken target's runner.
|
||||
me = pr(1, ["P3"])
|
||||
broken = pr(2, ["broken"], status=tc.RUNNING, run_ids=[200])
|
||||
self.assertEqual(tc.runs_to_cancel(me, [me, broken]), [])
|
||||
self.assertEqual(tc.runs_to_cancel(me, [me, broken]), [200])
|
||||
|
||||
def test_non_p0_self_reclaims_draft_lower_run(self):
|
||||
# A draft is not merge-ready — its run is likewise reclaimable by any higher PR.
|
||||
me = pr(1, ["P3"])
|
||||
draft = pr(2, [], draft=True, status=tc.QUEUED, run_ids=[200])
|
||||
self.assertEqual(tc.runs_to_cancel(me, [me, draft]), [200])
|
||||
|
||||
def test_broken_self_does_not_cancel_equal_broken(self):
|
||||
# Both effective P10 — the equal-or-higher invariant still forbids cancelling.
|
||||
me = pr(1, ["broken"])
|
||||
peer = pr(2, ["broken"], status=tc.RUNNING, run_ids=[200])
|
||||
self.assertEqual(tc.runs_to_cancel(me, [me, peer]), [])
|
||||
|
||||
def test_bottom_self_preempts_nothing(self):
|
||||
# A broken/draft PR (P10) is the bottom: nothing is strictly-lower, so it
|
||||
# cancels neither a higher (P5) nor an equal (P10) run.
|
||||
me = pr(1, [], draft=True) # P10
|
||||
prs = [
|
||||
me,
|
||||
pr(2, ["P5"], status=tc.RUNNING, run_ids=[200]), # higher
|
||||
pr(3, ["broken"], status=tc.RUNNING, run_ids=[300]), # equal P10
|
||||
]
|
||||
self.assertEqual(tc.runs_to_cancel(me, prs), [])
|
||||
|
||||
def test_p0_cancels_strictly_lower_active_runs(self):
|
||||
me = pr(1, ["P0"])
|
||||
@@ -203,6 +227,20 @@ class EndToEndDecisionTests(unittest.TestCase):
|
||||
self.assertEqual(dec.cancel_run_ids, ())
|
||||
self.assertTrue(dec.proceed)
|
||||
|
||||
def test_p5_reclaims_draft_then_waits_behind_higher(self):
|
||||
# A non-P0 PR can BOTH reclaim a broken/draft lower run (PASS 1) AND still
|
||||
# yield to a strictly-higher PR (PASS 2) in the same evaluation.
|
||||
me = pr(40, ["P5"], status=tc.RUNNING, run_ids=[4000])
|
||||
prs = [
|
||||
me,
|
||||
pr(41, ["P2"], status=tc.RUNNING, run_ids=[4100]), # higher — blocks
|
||||
pr(42, [], draft=True, status=tc.RUNNING, run_ids=[4200]), # draft — reclaimed
|
||||
]
|
||||
dec = tc.decide(me, prs)
|
||||
self.assertEqual(list(dec.cancel_run_ids), [4200])
|
||||
self.assertFalse(dec.proceed)
|
||||
self.assertEqual([b.number for b in dec.blockers], [41])
|
||||
|
||||
|
||||
class SnapshotParsingTests(unittest.TestCase):
|
||||
def test_from_json_label_objects_and_fields(self):
|
||||
|
||||
@@ -19,9 +19,10 @@ ORDER OF OPERATIONS (issue #342)
|
||||
1. Effective priority orders everything: the lowest-numbered `P0`-`P9` label present
|
||||
(P0 = highest), default `P5` if none. A `broken` OR `draft` PR is effectively P10
|
||||
(bottom, below P9), overriding any P0-P9 label.
|
||||
2. PASS 1 - preemption: **only a P0 (emergency) preempts.** A P0 cancels the
|
||||
in-progress / queued CI runs of ALL strictly-lower OTHER open PRs to reclaim their
|
||||
runners. P1-P9 never bump a lower run mid-flight.
|
||||
2. PASS 1 - preemption: a strictly-lower OTHER PR's in-progress / queued run is
|
||||
cancelled iff THIS PR is P0 (an emergency reclaims ALL lower runners) OR the target
|
||||
is broken/draft (a wasted run any higher-priority PR may reclaim). P1-P9 never bump
|
||||
a *normal* lower run mid-flight — only a P0 does that.
|
||||
3. PASS 2 - bounded hold-back: a non-P0 PR yields (cancels nothing) to any strictly-
|
||||
higher-priority OTHER PR that has an active/queued run, and — among its OWN
|
||||
priority level — to any PR ordered ahead of it (running-first, then oldest by
|
||||
@@ -169,20 +170,29 @@ def runs_to_cancel(
|
||||
*,
|
||||
self_run_id: int | None = None,
|
||||
) -> list[int]:
|
||||
"""PASS 1. Run ids to cancel — **empty unless THIS PR is P0**. A P0 preempts the
|
||||
active (running/queued) runs of every strictly-lower OTHER PR. Invariants: never
|
||||
cancel self (by number or run id), never cancel an equal-or-higher-priority PR."""
|
||||
if effective_priority(this_pr) != TOP_PRIORITY:
|
||||
return [] # only P0 preempts; P1-P9 never bump
|
||||
"""PASS 1. Run ids to cancel. A strictly-lower OTHER PR's active (running/queued)
|
||||
run is cancelled iff keeping it running is wasteful, i.e. EITHER:
|
||||
* THIS PR is P0 — an emergency reclaims every strictly-lower runner now; OR
|
||||
* the target is broken/draft (effective priority 10) — its run can't merge /
|
||||
isn't merge-ready, so ANY higher-priority PR may reclaim its runner.
|
||||
P1-P9 never cancel a *normal* strictly-lower run — they yield in PASS 2 instead.
|
||||
Invariants: never cancel self (by number or run id), never cancel an
|
||||
equal-or-higher-priority PR (only strictly-lower, prio > self)."""
|
||||
self_prio = effective_priority(this_pr)
|
||||
to_cancel: list[int] = []
|
||||
seen: set[int] = set()
|
||||
for pr in all_prs:
|
||||
if pr.number == this_pr.number:
|
||||
continue # never cancel self
|
||||
if effective_priority(pr) <= TOP_PRIORITY:
|
||||
continue # only strictly-lower (skip other P0s)
|
||||
target_prio = effective_priority(pr)
|
||||
if target_prio <= self_prio:
|
||||
continue # only strictly-lower (skip equal-or-higher)
|
||||
if pr.run_status not in ACTIVE:
|
||||
continue # nothing running/queued to cancel
|
||||
# Strictly lower: preemptible iff we're P0 OR the target is broken/draft
|
||||
# (a bottom, priority-10, wasted run that any higher PR may reclaim).
|
||||
if self_prio != TOP_PRIORITY and target_prio < BOTTOM_PRIORITY:
|
||||
continue # P1-P9 don't bump a *normal* lower run
|
||||
for rid in pr.run_ids:
|
||||
if self_run_id is not None and rid == self_run_id:
|
||||
continue # never cancel our own run
|
||||
@@ -377,13 +387,14 @@ def run_live() -> int:
|
||||
_log(f"This PR #{self_number} effective priority: {priority_label(self_prio)} "
|
||||
"(P0 = highest/emergency, P9 = lowest, broken/draft = bottom).")
|
||||
|
||||
# ── PASS 1: PREEMPTION (only a P0 self) ──────────────────────────────────
|
||||
# ── PASS 1: PREEMPTION (P0 reclaims all lower; anyone reclaims broken/draft) ──
|
||||
to_cancel = runs_to_cancel(this_pr, all_prs, self_run_id=self_run_id)
|
||||
if not to_cancel:
|
||||
if self_prio == TOP_PRIORITY:
|
||||
_log("P0 emergency — no strictly-lower active runs to cancel.")
|
||||
else:
|
||||
_log("Not P0 — no preemption (P1-P9 never cancel a lower run mid-flight).")
|
||||
_log("No preemptible runs (P1-P9 only reclaim broken/draft lower runs; "
|
||||
"none active).")
|
||||
else:
|
||||
cancelled = 0
|
||||
for rid in to_cancel:
|
||||
@@ -448,11 +459,14 @@ def run_dry(text: str) -> int:
|
||||
dec = decide(this_pr, all_prs, self_run_id=self_run_id)
|
||||
_log(f"This PR #{dec.self_number} effective priority: "
|
||||
f"{priority_label(dec.self_priority)}")
|
||||
if dec.self_priority == TOP_PRIORITY:
|
||||
_log(f"PASS 1 (preemption): P0 — cancel run ids: "
|
||||
f"{list(dec.cancel_run_ids) or '(none active)'}")
|
||||
if dec.cancel_run_ids:
|
||||
why = ("P0 emergency (reclaims all strictly-lower)"
|
||||
if dec.self_priority == TOP_PRIORITY
|
||||
else "reclaiming broken/draft lower runs")
|
||||
_log(f"PASS 1 (preemption): {why} — cancel run ids: "
|
||||
f"{list(dec.cancel_run_ids)}")
|
||||
else:
|
||||
_log("PASS 1 (preemption): not P0 — no cancellations.")
|
||||
_log("PASS 1 (preemption): nothing to cancel.")
|
||||
if dec.proceed:
|
||||
_log("PASS 2 (hold-back): PROCEED — no blockers.")
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user