From 75d88388f8ac3e5d63ec08b1fbafd035e2658864 Mon Sep 17 00:00:00 2001 From: jsboige Date: Tue, 27 Oct 2026 02:23:39 +0100 Subject: [PATCH] =?UTF-8?q?Fix(picker,#22636):=20le=20veto=20=C2=AB=20PR?= =?UTF-8?q?=20fusionnee=20<=2090=20j=20=C2=BB=20ne=20masque=20plus=20une?= =?UTF-8?q?=20consolidation=20au-dela=20de=2048=20h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le veto de la surface « PR MERGEE » (90 j) neutralisait la regle des 48 h pour la classe `consolidation` : une tranche mergee depuis plus de 48 h continuait de rendre la consolidation invisible pour 90 jours. - `delivered_signal_reason()` : nouveau parametre `skip_merged_pr`, le court-circuit est place AVANT l'appel de la sonde -- ni requete `gh`, ni consommation du budget de sonde, ni repli `UNPROBED`. - Boucle du tapis : pour la seule classe `consolidation`, l'age de la derniere livraison par PR est lu sur `last_delivery_stamp` (deja calcule en amont par le corpus `fetch_merged` partage, zero appel reseau supplementaire) ; au-dela de `CONSOLIDATION_LAG_H`, la sonde PR n'est plus appelee. Age inconnu -> comportement anterieur conserve (fail-closed). - Le LABEL `candidate-delivered` reste intact ; les grains ordinaires et les EPICs ordinaires sont inchanges. - 4 tests dans `scripts/tests/test_pick_idle_grain_22500_passe2.py` : consolidation 72 h servie (sonde PR NON appelee), consolidation 2 h retenue, grain ordinaire 72 h toujours retenu par la regle des 90 j, label retenu quel que soit l'age. Co-Authored-By: Claude Haiku 5.5 --- scripts/pick_idle_grain.py | 38 ++++++- .../test_pick_idle_grain_22500_passe2.py | 101 ++++++++++++++++-- 2 files changed, 126 insertions(+), 13 deletions(-) diff --git a/scripts/pick_idle_grain.py b/scripts/pick_idle_grain.py index 39dbed9d410..dcd11a37cd6 100644 --- a/scripts/pick_idle_grain.py +++ b/scripts/pick_idle_grain.py @@ -2023,6 +2023,7 @@ def delivered_signal_reason( failures: list[int] | None = None, merged_pr_probe=None, skip_marker: bool = False, + skip_merged_pr: bool = False, ) -> str | None: """Pourquoi ecarter ce candidat de l'urne `grain`, ou ``None``. @@ -2065,6 +2066,13 @@ def delivered_signal_reason( # de cloture a plus de 48 h reste ecartee par son LABEL ou par une PR # MERGEE recente (une consolidation accumule les PRs : ces deux surfaces # disent « un tranche est livree », pas « la consolidation est finie »). + # `skip_merged_pr` neutralise la surface « PR MERGEE » a son tour, pour + # la seule classe `consolidation` et seulement quand la derniere + # livraison par PR est DEJA CONNUE et depasse CONSOLIDATION_LAG_H : une + # PR fusionnee de plus de 48 h ne dit plus qu'une tranche vient d'etre + # livree, et la sonde n'a plus rien a mesurer. Le label reste intact, + # et la voie du tapis ne leve ce court-circuit qu'avec un horodatage + # connu -- sans lui, la sonde garde son comportement anterieur. if not skip_marker: verdict = probe(item["number"], lane) if verdict == DELIVERED_SIGNAL_UNPROBED: @@ -2091,6 +2099,11 @@ def delivered_signal_reason( # cycles de suite par le tapis narrow-cache. La sonde est partagee # avec `open_cover_reason` via le meme `_counted_probe` parent (meme # plafond `DELIVERED_SIGNAL_MAX_PROBES`). + # Court-circuit #22500 : consolidation deja datee de plus de 48 h -- + # ni requete `gh`, ni consommation du budget de sonde, ni repli + # UNPROBED qui renverrait le candidat sur le tapis sans le servir. + if skip_merged_pr: + return None merged = merged_pr_probe(item["number"], lane) if merged == DELIVERED_SIGNAL_UNPROBED: return None @@ -8104,11 +8117,27 @@ def claims_probe(numbers): # lecture d'evenement remplace le filtre recent pour cette classe. # Le marqueur n'ecarte plus au-dela de 48 h ; la surface # « commentaire » de la sonde pleine est neutralisee elle aussi - # (`skip_marker`), mais PAS les deux autres (label, PR MERGEE) -- - # une consolidation accumule les PRs, ces deux surfaces disent - # « une tranche est livree », pas « la consolidation est finie ». + # (`skip_marker`), le LABEL reste intact (il continue d'ecarter, + # quel que soit l'age), et la surface « PR MERGEE » est + # neutralisee seulement quand la derniere livraison par PR est + # deja connue et depasse 48 h (`skip_merged_pr`) : une + # consolidation accumule les PRs, et une tranche mergee depuis + # plus de 48 h ne dit plus « la consolidation est finie ». marker_expired = False + merged_pr_expired = False if is_consolidation(it): + # #22500 -- la surface « PR MERGEE » cesse elle aussi + # d'ecarter la consolidation au-dela de 48 h : le timestamp + # de la derniere livraison par PR est DEJA calcule en amont + # (`last_delivery_stamp`, corpus `fetch_merged` partage) -- + # le reutiliser evite une requete `gh` supplementaire. + # Age inconnu (aucune livraison dans la fenetre, ou corpus + # indisponible) -> la sonde garde son comportement anterieur. + delivery_age_h = _visit_age_hours( + it.get("last_delivery_stamp"), NOW) + merged_pr_expired = bool( + delivery_age_h is not None + and delivery_age_h >= CONSOLIDATION_LAG_H) verdict = (False if expired() else user_delivery_event_probe(n)) if verdict is None: @@ -8147,7 +8176,8 @@ def claims_probe(numbers): continue reason = delivered_signal_reason( it, args.lane, counted_probe, state["failures"], - counted_merged_pr_probe, skip_marker=marker_expired) + counted_merged_pr_probe, skip_marker=marker_expired, + skip_merged_pr=merged_pr_expired) if reason is not None: belt_withheld.append( (it, "LIVRAISON : " + reason diff --git a/scripts/tests/test_pick_idle_grain_22500_passe2.py b/scripts/tests/test_pick_idle_grain_22500_passe2.py index e34421ba617..61718f21972 100644 --- a/scripts/tests/test_pick_idle_grain_22500_passe2.py +++ b/scripts/tests/test_pick_idle_grain_22500_passe2.py @@ -68,10 +68,10 @@ def _free_claims(items): return {it["number"]: (pig.CLAIM_CODE_FREE, "libre") for it in items} -def _run(pool, *, event_probe=None, **kw): +def _run(pool, *, event_probe=None, probe_budget=8, **kw): return pig.belt_pick_with_replacements( pool, _free_claims(pool), _belt_args(grains=len(pool) + 2), - probe_budget=8, delivery_event_probe=event_probe, **kw) + probe_budget=probe_budget, delivery_event_probe=event_probe, **kw) # --- point 1 : une consolidation livree n'ecarte que 48 h ------------------ @@ -129,16 +129,99 @@ def test_le_marqueur_neutralise_ne_touche_pas_le_label(): assert withheld and "candidate-delivered" in withheld[0][1] -def test_le_marqueur_neutralise_ne_touche_pas_la_pr_mergee(): - """Meme chose pour la surface « PR MERGEE < 90 j » : une consolidation - accumule les PRs, son etat de livraison ne se lit pas dans le merge d'une - tranche. La regle des 48 h neutralise le marqueur, rien d'autre.""" +def test_consolidation_pr_mergee_72h_est_servie(): + """Une consolidation dont la derniere livraison par PR a plus de 48 h + n'est plus ecartee par la surface « PR MERGEE < 90 j » : une tranche + mergee depuis 72 h ne dit pas « la consolidation est finie ». + + L'age se lit sur `last_delivery_stamp`, deja calcule en amont par le + corpus `fetch_merged` partage : la sonde PR n'a alors plus rien a + mesurer, et elle ne doit meme pas etre appelee (une requete `gh` de + moins, et aucune consommation du budget de sonde). La sonde ci-dessous + rend True, c'est-a-dire le veto historique des 90 jours : sur la version + anterieure, elle etait appelee et la consolidation restait invisible.""" item = _consol(5005, visit_h=72) + appels = [] + + def _sonde_pr(n, lane=None): + appels.append(n) + return True + picks, withheld, _state = _run( [item], event_probe=lambda n: (True, True, _iso(72)), - merged_pr_probe=lambda n, lane=None: True) - assert picks == [] - assert withheld and "PR MERGEE" in withheld[0][1] + merged_pr_probe=_sonde_pr) + assert [p["number"] for p in picks] == [5005], ( + "la consolidation doit etre servie, " + f"withheld={[w[1] for w in withheld]}") + assert withheld == [] + assert appels == [], ( + "la sonde PR ne doit pas etre appelee pour une consolidation dont " + "la derniere livraison par PR depasse 48 h") + + +def test_consolidation_pr_mergee_2h_est_retenue(): + """Le veto « PR MERGEE » joue encore tant que la derniere livraison par + PR a moins de 48 h : une tranche vient d'etre mergee, la consolidation + n'est pas a re-servir. + + La lecture d'evenement rend ici un marqueur ANCIEN (72 h) alors que la + derniere PR date de 2 h : le filtre recent ne retient donc pas l'issue, + et c'est bien la surface PR seule qui est mesuree. Le grain libre en + second sert de temoin : la consolidation retenue est remplacee, et elle + n'est PAS re-servie derriere -- « retenue » veut dire qu'elle reste + invisible pour CE tirage, pas seulement qu'un motif est affiche.""" + item = _consol(5008, visit_h=2) + libre = _item(5011, visit_h=24 * 40) + pool = sorted([libre, item], key=lambda it: pig.belt_sort_key(it, now=NOW)) + picks, withheld, _state = _run( + pool, event_probe=lambda n: (True, True, _iso(72)), + merged_pr_probe=lambda n, lane=None: n == 5008, probe_budget=12) + assert [p["number"] for p in picks] == [5011], ( + "seul le grain libre doit etre servi ; la consolidation retenue ne " + "doit pas revenir comme remplacement") + reasons = [w[1] for w in withheld if w[0]["number"] == 5008] + assert reasons and all("PR MERGEE" in r for r in reasons), ( + f"la consolidation doit etre retenue par le veto PR : {reasons}") + + +def test_grain_ordinaire_pr_mergee_72h_est_toujours_retenu(): + """La regle des 90 jours ne change pas pour un grain ordinaire : une PR + mergee il y a 72 h reste un veto, meme quand le marqueur de commentaire + est ancien. La neutralisation des 48 h est propre a la consolidation. + + Le grain libre en second sert de temoin : le grain ordinaire retenu est + remplace sans etre re-servi derriere, exactement comme avant.""" + item = _item(5009, visit_h=72) + libre = _item(5012, visit_h=24 * 40) + pool = sorted([libre, item], key=lambda it: pig.belt_sort_key(it, now=NOW)) + picks, withheld, _state = _run( + pool, recent_marker_probe=lambda n: False, + merged_pr_probe=lambda n, lane=None: n == 5009, probe_budget=12) + assert [p["number"] for p in picks] == [5012] + reasons = [w[1] for w in withheld if w[0]["number"] == 5009] + assert reasons and all("PR MERGEE" in r for r in reasons) + + +def test_label_candidate_delivered_retenu_quel_que_soit_l_age(): + """Le label `candidate-delivered` n'est pas touche par la regle des 48 h : + il continue d'ecarter l'issue, quel que soit l'age de la derniere + livraison par PR, et sans qu'aucune sonde soit necessaire. Le grain + libre en second sert de temoin : l'issue labellisee est remplacee sans + etre re-servie derriere, a 2 h comme a 72 h.""" + for age in (2, 72): + item = _item(5010 + age, visit_h=age, + labels=["consolidation", "candidate-delivered"]) + libre = _item(5100 + age, visit_h=24 * 40) + pool = sorted([libre, item], + key=lambda it: pig.belt_sort_key(it, now=NOW)) + picks, withheld, _state = _run( + pool, event_probe=lambda n, a=age: (True, True, _iso(a)), + merged_pr_probe=lambda n, lane=None, a=age: n == 5010 + a, + probe_budget=12) + assert [p["number"] for p in picks] == [5100 + age], ( + f"age={age} h : le label doit retenir l'issue") + reasons = [w[1] for w in withheld if w[0]["number"] == 5010 + age] + assert reasons and all("candidate-delivered" in r for r in reasons) def test_une_lecture_en_echec_ne_neutralise_aucun_marqueur():