collector : une passe de rattrapage pauvre n'efface plus une passe riche #162
No reviewers
Labels
No labels
Compat/Breaking
EC01
EC02
EC03
EC04
EC05
EC06
Kind/BDD
Kind/Back
Kind/Bug
Kind/CICD
Kind/Cloud
Kind/Contenu
Kind/Data
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Front
Kind/IA
Kind/Infra
Kind/Monitoring
Kind/Security
Kind/Testing
Portée/Post-jury
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
ops/alerte
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
g2/enervision!162
Loading…
Reference in a new issue
No description provided.
Delete branch "lenaic/107-rattrapage-ne-degrade-pas"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
collector : une passe de rattrapage pauvre n'efface plus une passe riche
Ce qui a été mesuré aujourd'hui
La source applique l'état de panne courant d'un site à tout son historique.
La même URL, la même fenêtre, appelée trois fois à vingt secondes d'écart :
C'est pour cette raison que
docs/runbooks/collecteur.mdconseille de relancerle rattrapage plusieurs fois.
Le défaut
L'écriture visait la même clé sans rien comparer. Une passe rendant 2 points
remplaçait donc tranquillement une passe qui en avait rendu 22, et le conseil
« relancer jusqu'à ce que la liste se vide » supposait une amélioration monotone
que le code ne garantissait pas.
Le versionnement du seau gardait bien l'ancienne version, récupérable par
version_id. Ça ne suffit pas : la zone argent lit la version courante, etpersonne ne va parcourir des versions à la main pour retrouver la bonne.
Le correctif
Le protocole
Depotgagne une lecture,metadonnees_objet, et l'objet portedésormais
points-avec-valeur— écrit pour être relu par la passe suivante. Unstatsuffit à comparer, sans télécharger ni décompresser.Une comparaison impossible laisse écrire : clé absente, dépôt qui ne répond
pas, objet antérieur à cette métadonnée, ou dépôt qui ne sait pas lire. On
préfère une écriture de trop à une passe utile perdue.
Sur les deux cas existants qui changent
« L'idempotence tient à la clé » et « la même commande rejouée n'en crée pas
d'autres » restent vraies et restent vérifiées. C'est le compteur d'écritures
qui décrivait un effet de bord, et qui vaut maintenant un de moins quand la
seconde passe n'apporte rien. Cinq cas neufs couvrent le garde-fou, dont celui
qui motive tout le reste : une passe plus pauvre ne remplace pas celle en place.
Contexte utile
Six passes sur 30 jours ont été jouées aujourd'hui : ~5 fenêtres écrites sur
420 par passe, 0,0 % de points avec valeur. La source ne rend pas son
historique en ce moment — aucun site sain sur 8 essais en 96 secondes. Le
rattrapage n'est donc pas la voie pour l'historique d'entraînement ; ce correctif
sert le jour où la source se rétablit.
Preuve
pytest tests/unit: 601 tests, 0 échec.ruff,ruff format,mypy --strictsur le collecteur : propres.Suite des #107 et #111.
Revue
Le défaut est réel et bien cerné, et le correctif est le bon : comparer avant
d'écrire, sur une métadonnée qu'un
statsuffit à lire, et laisser écrire dèsque la comparaison est impossible. Les cinq cas neufs décrivent le garde-fou, et
la reprise des deux cas existants est honnête — c'est bien le compteur
d'écritures qui décrivait un effet de bord, pas l'idempotence.
Un défaut bloquant, corrigé sur la branche (
7f83b8c)Le garde-fou rendait la fenêtre avec
ecrit=False, ora_reprendrelit cedrapeau comme « pas d'objet en bronze » :
Une fenêtre gardée parce que bronze la portait déjà, et mieux, retournait donc
dans la liste « à rejouer », et
code_de_sortievalait 1. Mesuré en ajoutantl'assertion manquante au cas bout-en-bout existant, sur le code de la branche :
Deux exécutions de la même commande, la seconde sortait en 1 alors qu'elle
n'avait plus rien à faire, et la liste « à rejouer » ressortait entière. Le
manuel annonce 0 pour « tout ce qui était collectable est écrit » et conseille
de « relancer jusqu'à ce que la liste se vide » : cette liste ne se vidait plus
jamais. La demande cherchait à rendre ce conseil sûr ; en l'état elle le rendait
sans fin.
Lotgagne doncconserve, qui dit « bronze l'a, et mieux » là oùecritfauxdit seulement « rien n'a été écrit ». Les deux divergent — comme ils divergent
déjà sur un échec d'écriture, ce que la docstring de
a_reprendrenotait — etc'est précisément la distinction qu'elle demandait. Le résumé les compte à part :
0/420 fenêtres écrites, 420 déjà en placene se lit plus comme un échec.Une précision, parce que le message de ce commit va trop loin : il parle d'un
« cron nocturne » qui aurait échoué. C'est faux, et
vars.ymlle ditexplicitement —
rattrapage-readings.shn'est pas planifié et ne le sera pas.La portée réelle est la relance à la main, celle que le manuel décrit, et le
code de sortie sur lequel on s'appuie pour savoir s'il reste du travail.
Deux points de solidité, dans le même commit
lire(cle)était le seul appel au dépôt horstry.collecter_fenetrepromet de ne jamais lever et
rattraperl'appelle sans filet : une exceptionéchappée là n'aurait pas perdu une fenêtre mais toutes celles qui restaient.
L'adaptateur MinIO rattrape de son côté, donc rien ne casse en production —
mais le protocole
Depotautorise n'importe quel autre dépôt.int(brut)ne rattrapait queValueError; une métadonnée non textuelle donneTypeError. Le typage du protocole l'exclut, un dépôt tiers non.Une remarque, pour plus tard et pas pour ici
La clé n'encode que la fenêtre, jamais le pas (
cles.py). Un rattrapage relancéavec un
--pas-minutesplus fin vise donc la même clé, et si la source lui rendautant de points avec valeur qu'à la passe grossière, le
>=garde l'objetgrossier. Source rétablie, la passe fine en rend plus et gagne ; c'est le cas
dégradé qui pourrait surprendre. Comparer « points avec valeur » reste le bon
arbitre — c'est la grandeur qui porte le produit — donc je ne touche à rien,
mais ça vaut peut-être une ligne au manuel le jour où quelqu'un relance en plus
fin.
Vérification
Le manuel a été complété : le garde-fou y est décrit, et la lecture de
0/420 écrites, 420 déjà en placeavec un code de sortie 0 y est explicite.Rejoué sur la branche fusionnée avec develop :
pytest tests/unit918 tests0 échec,
ruff,ruff formatetmypy --strictpropres sur les six paquetstypés. En 3.14 et non en 3.12 — le serveur est inatteignable d'ici — donc c'est
la chaîne qui reste l'arbitre, et elle est verte.
Pour mémoire : la première passe de chaîne sur
7f83b8cavait été annulée envol, pas mise en échec. La fusion de la #166 dans develop a recalculé la
référence de la demande, et le groupe de concurrence du workflow annule la passe
la plus ancienne. Les quatre autres paliers étaient verts.