etl : agrégation et matérialisation de la zone or, export et lignage (#35) #124
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
g2/enervision!124
Loading…
Reference in a new issue
No description provided.
Delete branch "olivier/35-etl-zone-or"
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?
Ce que ça change
La zone or est alimentée : un service
services/etl/agrège la zone argent, la charge dans les hypertables, en produit l'export de reporting et sait remonter n'importe quelle valeur affichée jusqu'à sa lecture bronze. La migration0016matérialise les deux agrégats que le §12 annonçait sans qu'ils existent en base.Le critère 1 du ticket ne demandait plus qu'une zone argent réelle : le #34 est fusionné, la branche est à jour, et les deux zones vivent maintenant dans un seul paquet —
etl/silver/etetl/gold/, un seulconfig.py. Le job lit le schéma que la zone argent produit réellement, et les fixtures sont dérivées de son schéma déclaré au lieu de le recopier.Avance #35
Preuve
Rejouée sur
e974508, branche à jour dedevelop:Les 251 cas de
tests/unit/{gold,silver,db}passent ensemble : c'est le premiermoment où les deux zones sont exercées dans la même passe.
Les tests d'intégration de
tests/integration/gold/test_chaine_zone_or.pyjouent la chaîne entière sur une journée de 1 440 minutes pour deux sites : agrégation,qualite_jour, export horaire, reconstitution de l'export à la mesure près, et remontée d'un échantillon de 20 lignes tirées au hasard jusqu'à l'objet bronze. Sans MinIO, sans base, sans réseau — les fixtures de zone argent sont construites en Parquet local.Ce qui n'est pas encore prouvé, et qui ne peut pas l'être ici : la latence ENF-04 sur la zone or réellement chargée et la remontée depuis la base. Les deux cas sont écrits (
test_chargement_postgres.py) et se sautent faute de base joignable. Ils demandent que les migrations0008–0016soient appliquées, ce qui est le lot de la #114. La chaîne de préparation du jeu de latence, elle, est jouée à blanc sans base : 70 560 lignes de zone argent construites, projetées et contrôlées.Relecture
Suite à la relecture, un commit par point
etlau même chemin, et le même garde-fou sous deux variablesetl/silver/+etl/gold/, un seulconfig.py, un seulpyproject.tomlà quatre dépendances,ENERVISION_ETL_TAUX_TROUS_MAXpour les deux zonesc254f26COLONNES_ARGENTa dérivé (23 colonnes contre 38)silver.entrepot.SCHEMASet écrit par le puits de la zone argent lui-même ; un garde-fou refuse une ligne qui s'en écartefc453cfqualite_jourmatérialisedisponibilite_jourau lieu de le recalculer ; le caveat du site muet disparaît, le réglage de cadence aussi6aaed09mesure_horaire(l'objet que l'API servira), sept jours × sept sites, quatre lecteurs simultanésd62d462e974508Trois défauts que ces retours ont fait apparaître, et qu'ils n'avaient pas
demandés : la fenêtre de latence ne couvrait que les heures écoulées depuis
minuit et non 24 h ;
refresh_continuous_aggregaterefuse d'être appelé dansune transaction, donc le cas de concordance de l'agrégat aurait échoué à sa
première exécution réelle ; et
CadenceIncoherenten'était pas attrapée parmain(), qui rendait 1 là où le README annonce 4.Ce qui suit le code
.env.exampleToutes sous le préfixe
ENERVISION_ETL_, avec un défaut utilisable partout sauf pour les identifiants MinIO et l'URL de la base ; les lanceurs sourcent les mêmes/etc/enervision/minio.envetpostgres.envque le collecteur. Le tableau complet est dansservices/etl/README.md, la configuration lue nulle part ailleurs que dansetl/config.py.docs/data/etl-pipeline.md§12 est réécrit sur ce qui est posé, son §14 referme le point ouvert « chargement gold → PostgreSQL non spécifié », le tableau d'exposition Grafana dedocs/POSTGRESQL.mdpasse les deux nouveaux objets au même crible table par table que les sept du #105, etdb/migrations/README.mddocumente le motif de l'agrégat continu.Pas de fiche de décision, et la relecture a confirmé pourquoi : l'ADR 0011 se termine par « une agrégation pure, sans règle, n'a aucune raison de quitter le SQL ». Elle est citée en tête de la 0016. Le choix de forme, lui, reste réversible (les deux objets se suppriment sans rien perdre,
public.mesurereste la seule source) et motivé dans le fichier SQL.Où regarder en priorité
1. Le choix de la forme des agrégats —
db/migrations/0016_zone_or_agregats.sql, en tête de fichier. Le §12 décrivaitmesure_horaireetcharge_sitecomme des tables ; elles deviennent un agrégat continu TimescaleDB et une vue. L'alternative écartée est écrite, et l'ADR 0011 citée. @marvin, c'est toi qui les liras depuis l'API (#38) : c'est le seul point de cette demande qui attend encore une réponse, et le relevé de latence porte désormais surmesure_horaire, donc sur ce que tu serviras.2. La zone or reste au pas de la minute, et c'est délibéré —
services/etl/etl/gold/agregation.py. Un agrégat horaire à la place de la série aurait coupé le lignage à l'endroit exact où l'ENF-07 le demande : c'est l'identité de la clé(site_id, horodatage)de bronze à or qui rend le contrôle par échantillon possible.3. Le garde-fou de la fenêtre de réécriture —
etl/gold/chargement.py.mesureest comprimée au-delà de sept jours ; unon conflict do updatequi y retombe décomprime les segments touchés, pour 35 Mo relevés le 3 septembre sur préproduction. Le chargement refuse ces jours-là sauf--forcer. Si quelqu'un trouve la fenêtre trop stricte pour un rattrapage, c'est le paramètreENERVISION_ETL_FENETRE_REECRITURE_JOURS.4. Trois pièges attrapés en écrivant, qui valent une lecture — DuckDB rendait les horodatages dans le fuseau de la machine, donc le même jour exporté depuis un poste en CEST et depuis le serveur en UTC donnait deux fichiers différents (
SET TimeZone = 'UTC'dansgold/duck.py) ;pytzest une dépendance qu'aucun fichier n'importe et qu'un cas de test garde, sinon quelqu'un la retirera comme inutilisée ; la lecture en partitionnement Hive s'active toute seule et ajoutedomain,tableetdtaux colonnes du fichier.Deux choses hors de ce lot, à ne pas perdre
docs/dataest ignoré par.gitignore: la règledata/matche à tous les niveaux. Le fichier existant reste suivi, mais tout nouveau document déposé là serait silencieusement invisible. Le correctif tient en un caractère (/data/), hors périmètre de ce ticket.Quatre entrées, dans un service nouveau : `services/etl/` n'existait pas, le README racine n'attribuait aucun chemin à l'agrégation, et la mettre sous `services/collector/` aurait fait du collecteur « collecte + imputation + agrégation + chargement ». etl.agregation zone argent → zone or, `qualite_jour` comprise etl.chargement zone or → `mesure` et `qualite_jour`, en upsert etl.export export de reporting horaire, et sa reconstitution etl.lignage contrôle ENF-07 : remonte un échantillon jusqu'au bronze **La zone or n'est pas un agrégat de plus.** `public.mesure` porte la même série au pas de la minute que la zone argent, sur la même clé, sans table de correspondance : c'est cette identité de bout en bout qui rend l'ENF-07 vérifiable par échantillon. Un agrégat horaire à la place aurait coupé le lignage à l'endroit exact où l'exigence le demande. Les agrégats sont dérivés, en base, et c'est la migration 0016 qui les pose. **Le chargement refuse les jours déjà comprimés.** Un `on conflict do update` qui retombe dans un fragment comprimé décomprime les segments touchés, pour un coût sans rapport avec le nombre de lignes chargées — 35 Mo pour 5,4 Mo, relevé le 3 septembre sur `enervision_preprod`. Recharger un vieux jour reste possible, avec `--forcer` : c'est un geste délibéré, pas un effet de bord d'un job de cron. Le message porte la commande de décompression. **Trois choses que la mise en place a révélées, et qui valent plus que le code.** 1. DuckDB rendait les horodatages dans le fuseau de la machine. Le même jour exporté depuis un poste en CEST et depuis le serveur en UTC donnait deux fichiers différents — ce qui suffit à casser le « reconstituable à la mesure près » du ticket. `SET TimeZone = 'UTC'` est posé dans la connexion. 2. `pytz` est une dépendance que rien n'importe. DuckDB en a besoin pour rendre un `TIMESTAMPTZ` en objet Python : sans lui, le job échoue au `fetchall` du chargement, donc après l'agrégation, sur une machine où tout paraissait installé. Un cas de test garde la dépendance, sinon quelqu'un la retirera comme inutilisée. 3. La lecture en partitionnement Hive s'active toute seule dès qu'un segment de chemin ressemble à `clé=valeur`, et ajoute `domain`, `table` et `dt` aux colonnes du fichier. Le chargement nomme donc ses colonnes une par une. Le job échoue plutôt que d'écrire une journée douteuse : au-delà de 30 % de trous (§11), sur une valeur d'énumération que la base refuserait, ou sur plus de relevés disponibles que la cadence n'en permet — un doublon de zone argent donnerait sinon un taux de disponibilité au-dessus de 100 %. 62 cas unitaires, 91 % de couverture sur le service, plus la chaîne entière sur une journée de 1 440 minutes dans `tests/integration/gold/` — le répertoire que le ticket nomme. Les fixtures de zone argent sont construites en Parquet local et non versionnées : `.gitignore` exclut `*.parquet`, et un binaire de test que personne ne peut relire est un binaire que personne ne corrige. Le job d'agrégation lit le schéma du §10, qui est figé : il tourne donc dès que le #34 livre une zone argent réelle, sans rien changer ici.Relu en entier, code en main, depuis la branche du #34. Le travail est solide et la sémantique colle à la zone argent sans que rien n'ait été concerté — c'est plutôt bon signe. Un point de structure à trancher avant fusion, trois points à reprendre, et deux questions.
Ce qui s'aligne déjà tout seul
Les trois énumérations de etl/config.py sont identiques au caractère près à ce que la zone argent produit : measured / interpolated / forward_fill / none, good / imputed / suspect / critical, good / partial / degraded / critical. Aucune couche de traduction ne sera nécessaire. SQL_PROJECTION ne lit que des colonnes réellement écrites par le #34 — y compris après le changement de schéma qu'il a subi en relecture, qui est purement additif.
SET TimeZone = 'UTC' dans duck.py rejoint le §8.4, le garde-fou à 30 % existe des deux côtés avec le même défaut, et RELEVES_ATTENDUS_PAR_JOUR = 1440 correspond exactement à la grille régularisée.
Sur la question posée en point 1 — la forme des agrégats mérite-t-elle une fiche ? L'ADR 0011, introduite par le #34, se termine par : « Une agrégation pure, sans règle, n'a aucune raison de quitter le SQL. » La décision est déjà tracée, il suffit de la citer en tête de 0016. Pas de nouvelle fiche à écrire.
1. Bloquant : deux paquets etl différents au même chemin
Les deux PR créent services/etl/etl/. Quatre fichiers en conflit dur, avec des contenus incompatibles : etl/init.py, etl/pyproject.toml, etl/config.py, services/etl/README.md. Et les dispositions divergent : le #34 range la zone argent dans etl/silver/, cette PR pose ses modules à plat dans etl/. Après fusion on aurait etl/agregation.py à côté de etl/silver/, ce qui ne se lit pas.
La forme qui marche, et que le #34 avait anticipée en sortant config.py du sous-paquet : etl/silver/ + etl/gold/, un seul config.py, un seul pyproject.toml réunissant les quatre dépendances.
Sous-problème du même ordre — le même garde-fou sous deux variables :
Même préfixe, même concept, même défaut, deux noms. Un exploitant en règlera une et s'étonnera que l'autre job ne bouge pas.
2. COLONNES_ARGENT a déjà dérivé, et le docstring l'avait prédit
tests/conftest.py dit très justement qu'« une fabrique recopiée dérive de la table qu'elle est censée imiter ». C'en est une, et elle a dérivé par rapport à ce que le #34 écrit :
Ce n'est pas bloquant pour le job — la projection ne lit que l'intersection. C'est la fabrique qui ment sur ce qu'elle couvre. Le correctif propre, une fois les deux fusionnés : la dériver de etl.silver.entrepot.SCHEMAS[TABLE_MESURE], que le #34 expose déjà pour cette raison exacte et garde par un test anti-dérive.
3. L'EF-06 est calculé deux fois, sur deux dénominateurs
#34 silver/disponibilite_jour │ grille régularisée │ 1440 minutes, trous compris │
#35 public.qualite_jour │ lignes présentes en argent │ releves_attendus │
qualite.py documente honnêtement sa limite : « Un site totalement absent de la zone argent n'y produit aucune ligne […] le taux de 0 % qu'il mériterait n'apparaît nulle part. » Cette limite disparaît dès que le #34 est fusionné : la régularisation de la grille garantit 1440 lignes par site et par jour, même sans une seule collecte — c'est précisément ce qu'elle sert à rendre visible. Le caveat sera donc à supprimer, pas à conserver.
Reste à décider qui porte l'EF-06 au §15, et si qualite_jour recalcule ou lit disponibilite_jour. Recalculer est défendable (la base doit être autoportante), mais alors les deux dénominateurs doivent être documentés comme équivalents, sinon deux chiffres circuleront pour la même exigence.
Sur les critères de ton ticket, les critères 3 et 4 sont tenus. Le 1 le sera mécaniquement quand le #34 sera fusionné et la 0016 appliquée (donc en théorie rien à changer dans le code). Le 2 est le seul qui pourra poser problème au niveau de tes preuves à fournir.
Le test est bien construit (20 exécutions, p95, relevé horodaté non versionné) mais son chiffre ne peut pas servir de preuve, pour trois raisons :
Le code tient le critère, la migration aussi ; seule la mesure ne le démontre pas encore.
Point de coordination : la #123 de Justine est approuvée et part en premier. Elle crée
services/etl/de son côté, comme toi.Vos deux demandes se marchent dessus sur cinq fichiers, en conflits add/add donc à
résoudre à la main :
Rien de grave, mais ce n'est pas un rebase automatique : il faudra fusionner les deux
config.pyet les deuxpyproject.tomlplutôt que d'en choisir un.Ta #124 reste par ailleurs en état, sa chaîne est verte et ma relecture n'avait que deux
remarques mineures.
Le critère 2 du ticket demande la latence de lecture d'une fenêtre de 24 h sous charge légère. Le cas était bien construit — vingt exécutions, p95, relevé horodaté non versionné — mais son chiffre ne pouvait pas servir de preuve, pour les trois raisons que Justine a relevées. Elles sont traitées dans l'ordre de leur importance, qui n'est pas celui où elles se voient. **1. L'objet mesuré.** La requête recalculait un `time_bucket` sur `public.mesure`, alors que sa définition est **exactement** celle de `public.mesure_horaire`, l'agrégat continu que la 0016 livre dans cette même demande. Un chiffre relevé sur un objet que l'API ne servira pas ne dit rien de ce que l'utilisateur attendra. Deux relevés sont maintenant exigés, un par vue du tableau de bord : Parc entier, agrégat horaire public.mesure_horaire Un site, série à la minute public.mesure Le recalcul direct reste mesuré et imprimé, **sans être exigé** : l'écart entre lui et l'agrégat est ce qui justifie la migration 0016, et un lecteur du ticket doit pouvoir le vérifier au lieu de me croire. **2. Le volume.** 2 880 lignes mettaient le résultat sous la cible d'avance. Le jeu passe à sept jours et aux sept sites du référentiel, soit 70 560 lignes chargées par le fichier lui-même — indépendamment de ce que la base porte déjà — et le relevé imprime les deux comptes : le total de `public.mesure` et la part qui tombe dans la fenêtre mesurée. Deux journées auraient suffi à ce que la fenêtre de 24 h soit **pleine**, et c'est un défaut que je n'avais pas vu : elle commence en milieu de journée d'hier, la version précédente ne chargeait qu'aujourd'hui, donc ne mesurait que les heures écoulées depuis minuit. **3. La concurrence.** Quatre lecteurs simultanés, chacun sur sa connexion, chacun ses vingt exécutions, durées mises en commun. Quatre parce que le tableau de bord a une poignée d'utilisateurs et que le serveur partage 8 Go : au-delà on mesurerait la machine. **Un défaut de la base attrapé en écrivant :** `refresh_continuous_aggregate` ne s'exécute **pas** dans une transaction, et psycopg en ouvre une dès le premier ordre. Le cas de concordance de l'agrégat l'appelait sur la connexion ordinaire : il aurait échoué à la première exécution contre un vrai PostgreSQL, sur un message qui ne parle pas de transaction. Une seconde connexion en `autocommit` lui est dédiée. Un cas s'ajoute au passage, que les sept sites rendent possible : le taux de charge de **chaque** site reste sous sa capacité déclarée. Tant que le jeu ne portait que SITE001 et SITE002, un forfait de 600 kW sur SITE006 — 180 kW au référentiel — aurait donné un taux de 330 % sans que rien ne rougisse. Ces cas se sautent toujours faute de base joignable depuis ce poste : le chiffre reste à relever sur la préproduction, une fois les migrations 0008–0016 appliquées (#114). Ce qui est vérifié ici est ce qui peut l'être — la chaîne de préparation du jeu, jouée à blanc : 70 560 lignes de zone argent construites, projetées et contrôlées, sept lignes de qualité par jour à 1 440 relevés attendus et 32 manquants. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Merci à tous les deux — la relecture a trouvé un vrai bloquant et trois vrais
défauts. Tout est traité, un commit par point, et la branche est à jour de
develop(la #123 est passée entre-temps, donc les cinq conflits add/add sontrésolus et la demande est de nouveau
mergeable).etlau même cheminc254f26(merge)COLONNES_ARGENTa dérivéfc453cf6aaed09d62d462e9745081. Bloquant — un seul paquet,
etl/silver/+etl/gold/Résolu dans le sens que tu proposes, et que le #34 avait anticipé en sortant
config.pydu sous-paquet : un seuletl/config.py, un seulpyproject.tomlréunissant les quatre dépendances (
duckdb,minio,psycopg,pytz), mesmodules descendus dans
etl/gold/. Les entrées deviennentpython -m etl.gold.<module>— lanceurs de cron, README et §12 suivis. Ettests/unit/etl/devienttests/unit/gold/, en regard detests/unit/silver/.Sur le plancher DuckDB, j'ai pris le tien vers le haut :
>=1.5, la versionépinglée dans
requirements-dev.txt(1.5.5). Deux planchers pour un seul moteurinstallé se contredisent au premier
pip install.Le garde-fou sous deux noms :
ENERVISION_ETL_TAUX_TROUS_MAXIMALdisparaît,ENERVISION_ETL_TAUX_TROUS_MAX(le tien) sert les deux zones. Tu avais raisonsur la conséquence : un exploitant en aurait réglé une et se serait étonné que
l'autre job ne bouge pas.
Un détail de disposition : les champs propres à la zone or portent un défaut
dans
Settings, pour que les doubles detests/unit/silver/continuent deconstruire un
Settings(...)à neuf champs sans rien savoir de la zone or.Et un cas de plus, qui vient de ta première remarque : tu as constaté à la
main que les trois énumérations de
config.pyétaient identiques à ce que lazone argent produit. C'est maintenant un test — les constantes sont lues dans
silver.imputationetsilver.mesure, jamais recopiées. Elles l'étaient déjàcontre la migration 0010 : la zone or est tendue entre ces deux bords, et un
régime ajouté d'un seul côté ferait refuser en bloc une journée d'argent
parfaitement valide.
2. La fabrique avait dérivé, et le docstring l'avait prédit
23 colonnes contre les 38 que
silver.mesureécrit. Tu as raison sur le fondcomme sur le correctif : plus rien n'est recopié.
etl.silver.entrepot.SCHEMAS;PuitsDuckDBlui-même, dont seule la cible estdétournée vers le disque — même
CREATE TABLE, même ordre de colonnes, mêmeCOPY;silver.qualite.disponibilite_du_jour;_verifier_couverturerefuse une ligne qui s'écarte du schéma :PuitsDuckDB.ecrirelit parligne.get(nom), donc sans ce garde-fou unecolonne ajoutée en argent redeviendrait un
NULLsilencieux.Un effet de bord que la dérive cachait, et qui valait la peine : l'ancienne
fabrique écrivait par
PARTITION_BY (dt), ce qui retiredtdu fichier.La zone argent, elle, écrit l'objet à son chemin exact et garde
dtdans lefichier — donc la lecture de la zone or voyait
dtdeux fois en production etjamais en test. DuckDB 1.5 résout ce doublon en faveur de la partition
(vérifié), donc rien n'était cassé, mais ce n'était pas vérifié. La fabrique
écrit maintenant comme toi.
Le référentiel des sept sites (type, capacité) est celui de la migration 0009,
et un cas le garde. La consommation du jeu d'essai vaut 60 % de la capacité
du site : les deux valeurs en dur — 120 kW pour SITE001, 600 pour SITE002 —
étaient déjà exactement cela, mais la règle tient pour les cinq autres, dont
SITE006 (180 kW) qu'un forfait de 600 kW aurait mis à 330 % de charge.
3. L'EF-06 — la zone argent le porte, la zone or le sert
Tranché dans le sens que ton §10 annonçait déjà : « la zone or le matérialisera
sous
qualite_joursans le recalculer ».gold.qualitelitdisponibilite_jouret le projette au schéma de la table :Les chiffres ne changent pas, et c'est vérifiable : en argent une minute non
imputable porte
none, en or la même ligne portecritical. Les deuxdéfinitions du « relevé disponible » coïncident exactement.
Le caveat est supprimé, pas conservé — tu avais raison. Un site muet arrive
maintenant à 0 %, et un cas le vérifie.
Ce qui reste calculé en or est la répartition par méthode, que la zone
argent ne publie pas ; ses totaux sont confrontés à l'indicateur par une
jointure externe des deux côtés.
IndicateurDiscordantremplaceCadenceIncoherenteet attrape la même faute (déduplication du §8.2 non jouée)plus celles que l'ancien contrôle ne voyait pas : un site présent dans une seule
des deux tables, un indicateur calculé sur une autre journée.
Deux conséquences :
ENERVISION_ETL_RELEVES_ATTENDUSdisparaît. Plus personne ne le lisait, etun réglage que rien ne lit est un piège : on le règle, et rien ne bouge. Le
dénominateur vient de la seule zone qui sait combien de minutes la journée
comptait. La fixture
settings_journeedisparaît avec lui.CadenceIncoherenten'était pas attrapée parmain(): un exploitantaurait eu une trace d'appels et le code 1, là où le README annonce 4. Corrigé.
Le §15 dit maintenant qui porte quoi : « calculé une seule fois en argent (§10),
matérialisé sans recalcul en or (§12) ».
Critère 2 — le relevé de latence
Tes trois raisons sont justes, et la troisième est la plus grave : je mesurais un
objet que l'API ne servira pas. Traitées dans cet ordre.
L'objet. Deux relevés sont maintenant exigés, un par vue du tableau de bord :
le parc à l'heure sur
public.mesure_horaire(l'agrégat continu de la 0016), etun site à la minute sur
public.mesure. Le recalcul direct dutime_bucketreste mesuré et imprimé sans être exigé : l'écart entre lui et l'agrégat est
ce qui justifie la migration, et un lecteur du ticket doit pouvoir le vérifier
au lieu de me croire.
Le volume. Sept jours × sept sites = 70 560 lignes chargées par le fichier
lui-même, et le relevé imprime les deux comptes — le total de
public.mesureetla part qui tombe dans la fenêtre. Au passage, ta remarque en a révélé une
autre : la fenêtre
now() - 24 hcommence en milieu de journée d'hier, et je nechargeais qu'aujourd'hui. Je ne mesurais donc même pas 24 h, seulement les
heures écoulées depuis minuit.
La concurrence. Quatre lecteurs simultanés, chacun sur sa connexion, chacun
ses vingt exécutions, durées mises en commun. Quatre parce que le tableau de
bord a une poignée d'utilisateurs et que le serveur partage 8 Go — au-delà, on
mesurerait la machine.
Un défaut de la base attrapé en écrivant :
refresh_continuous_aggregatenes'exécute pas dans une transaction, et psycopg en ouvre une dès le premier
ordre. Le cas de concordance de l'agrégat l'appelait sur la connexion
ordinaire : il aurait échoué à la première exécution contre un vrai PostgreSQL,
sur un message qui ne parle pas de transaction. Une connexion en
autocommitlui est dédiée.
Ce qui n'est toujours pas prouvé, et c'est la seule chose qui reste. Le
chiffre demande une base joignable avec les migrations
0008–0016appliquées,donc la #114. Ce que j'ai pu vérifier depuis ce poste est la chaîne de
préparation du jeu, jouée à blanc sans base : 70 560 lignes de zone argent
construites, projetées et contrôlées en 130 s, sept lignes de qualité par jour à
1 440 relevés attendus et 32 manquants. Qui peut me dire si les migrations
sont passées en préproduction ? Je joue le relevé et je le colle ici dans
l'heure.
La fiche de décision
Tu as raison, et la citation est la bonne : « une agrégation pure, sans règle,
n'a aucune raison de quitter le SQL ». Pas de nouvelle fiche. L'ADR 0011 est
citée en tête de la 0016, avec la distinction qui reste utile : le pourquoi en
SQL est tracé par la fiche, la forme — agrégat continu plutôt que table — ne
l'est pas et n'a pas à l'être puisqu'elle est réversible. Le point ouvert du §14
renvoie à la fiche et ne demande plus que la confirmation de la forme.
Un report de la renumérotation du #34 au passage : le titre de la fiche portait
encore « ADR 0010 », comme la fiche du tunnel SSH, alors que l'index en
distingue bien 0010 et 0011. Corrigé, puisque la migration la cite maintenant
par son numéro — dis-moi si tu préfères le porter toi-même.
@lenaic
Les cinq fichiers sont fusionnés et non choisis, comme tu le demandais : les
deux
config.pyn'en font qu'un (un seulSettings, un seul nom pour legarde-fou de trous), les deux
pyproject.tomlréunissent les quatredépendances, et le §2 du document data était en plus dédoublonné — la fusion de
la #123 y avait laissé trois lignes en double.
Deux choses pour toi :
commentaires de la demande, ni en relecture attachée. Peux-tu les reposter ?
Je préfère les traiter dans ce lot que les perdre.
services/etl/etl/golddans les zones sensibles de la chaîne ? Lesous-paquet est à 91 % aujourd'hui, donc au-dessus du palier de 85 %.
Je ne touche pas à
ci.ymlsans ton avis : c'est ton fichier, et l'ADR 0011ne nomme que la zone argent.
Et une divergence que je laisse en dehors de ce lot parce qu'elle touche les deux
zones : sur une journée refusée, la zone argent rend 1 et la zone or 4.
Le README du service le dit noir sur blanc en attendant qu'un lot aligne les
deux — ça se fera bien quand les lignes de crontab existeront pour de vrai.
Preuve
Les 251 cas de
tests/unit/{gold,silver,db}passent ensemble, ce qui est lepremier moment où les deux zones sont exercées dans la même passe.
Deuxième tour, mais pas de relecture :
developa bougé sous la brancheLes #142 et #143 (tes #135 et #136, @lenaic) sont passées pendant que je
traitais les retours. Les deux touchent ce que la zone or lit, donc j'ai
refusionné et corrigé. Deux commits :
958bd39(merge) etdcc76db.Une bonne nouvelle d'abord : ta colonne
journee_completeest arrivée dansdisponibilite_jouret aucune ligne de test n'a eu à changer. La fabriquedérive du schéma que
silver.entrepotdéclare depuisfc453cf— c'estexactement ce que le point 2 de @justine devait produire, et c'est la première
fois que ça se vérifie sur un vrai changement de schéma.
Le raccordement des deux zones dans le temps était cassé
Pas par ton lot, par le mien : mes lanceurs passaient
date -u +%F. Avec larègle du #135, la chaîne se serait trompée deux fois par nuit :
clôt la veille, et la journée du jour n'est écrite qu'à 01 h 00 :
gold_dailysortait en « partition absente », chaque nuit, code 3 — dans un journal que
personne ne lit tant qu'un tableau de bord n'est pas vide ;
date tournait à 23 h 17, sur une grille arrêtée à 22 h 59. La dernière heure
de chaque journée n'aurait jamais atteint la zone or, et
qualite_jourgarderait pour toujours
releves_attendus = 1380. Plausible, donc invisible.etl/gold/jour.pyporte maintenant la règle pour les quatre entrées, et ellet'est demandée plutôt que recopiée : c'est
silver.grille.minutes_attenduesqui sait à quelle heure une journée n'a aucune minute révolue. Si tu déplaces ce
seuil, la zone or suit.
--jourdevient facultatif, les lanceurs ne calculentplus de date, et six cas à horloge fixe gardent la règle — minuit et fuseau
local compris.
Tes deux garde-fous du #136 sont adoptés dans
gold-daily.shetload-postgres.sh:ENERVISION_PYTHON, et le contrôle des clients avant delancer.
gold-dailyveut DuckDB,load-postgresDuckDB et psycopg — même modede panne que celui que tu as relevé, l'import tardif qui laisse le module
s'importer et casse au milieu du job.
Trois choses pour toi, @lenaic
taches_planifieessait porter un autre service depuis le #136 — c'est toi qui as sorti
services/collectordu chemin en dur. Il n'y manque que deux entrées,gold-daily.shà la minute 17 etload-postgres.shà la 27. Je ne les aipas ajoutées : tu travailles dans ce rôle en ce moment et je préfère un
conflit en moins. Dis-moi si tu les prends ou si je pousse les deux lignes.
e974508, « Python — qualité, tests et dépendances » passe en 2 min 58, et« Tableau de bord » a été annulé à 09:04:05 — à la seconde où les
exécutions de la #121 démarraient. Les deux premières tâches de la #121 ont
été annulées de la même façon. Hypothèse :
concurrency.groupvaut${{ github.workflow }}-${{ github.ref }}, etgithub.refne distingue pasdeux demandes de fusion sur un événement
pull_request— auquel cas toutesles demandes partagent un seul groupe et s'annulent entre elles avec
cancel-in-progress: true. À vérifier de ton côté, je n'ai pas touché àci.yml.Pour @justine
Une conséquence de ton point 3 que ton #135 rend visible : puisque le
dénominateur vient de chez toi,
qualite_jour.releves_attendusvaut les minutesécoulées tant que la journée n'est pas révolue, et 1 440 après la passe de
minuit. Je ne reprends pas
journee_completeen base — la migration 0013n'a pas la colonne, et
releves_attendus < 1440dit la même chose. C'est écritau §12, et c'est à trancher avec @marvin s'il préfère un drapeau explicite à une
comparaison : ce serait une 0017, pas ce lot.
Preuve, rejouée sur
dcc76dbLa chaîne sur
dcc76db: le job « Tableau de bord » échoue, et ce n'est pas ce lotCe que je peux affirmer, et sur quoi je m'appuie :
L'arbre du tableau de bord est identique, au bit près, à celui de
develop.Le même objet git sur les trois références — mon sommet,
develop(eb12df2),et le
developsur lequel ce même job a réussi il y a une heure(
d043bbe) :Mon lot ne touche ni
.forgejo/, niservices/dashboard, ni unpackage-lock.json. Les seuls fichiers que ce job lit et que je pourraisinfluencer sont le workflow et le CA épinglé, intacts tous les deux.
Et tout ce que ce job fait passe sur mon poste, sur ce même sommet :
Restent, dans le conteneur et hors de ma portée :
npm cicontre le registre,le cache npm de
actions/cache, ounpm sbom. Les 7 min 17 avant l'échecpointent vers l'installation plutôt que vers les tests, qui prennent moins d'une
seconde.
Ce que je ne peux pas faire : lire le journal du job. L'API de la forge
n'expose pas les journaux d'exécution dans cette version (404 sur
actions/jobs/{id}/logs,actions/runs/{id}/jobsetactions/tasks/{id}/logs) — il faut l'interface web.@lenaic, deux demandes, dans l'ordre :
c'est bien
npm ci, une relance suffit ; si c'est le cache, la clé estnpm-${{ hashFiles('services/dashboard/package-lock.json') }}et elle estpartagée par toutes les branches.
vide — ce que je préfère éviter sur une branche en relecture.
Rappel de l'exécution précédente (
e974508), pour le cas où les deux seraientliées : ce même job avait été annulé à la seconde où les exécutions de la
#121 démarraient, en même temps que les deux premières tâches de la #121
elles-mêmes. L'hypothèse du groupe de concurrence partagé entre demandes de
fusion tient toujours, et elle expliquerait qu'un job soit tué au milieu d'un
npm ci— ce qui laisse un cache à moitié écrit.Correction : c'est le #148, et il est déjà ouvert
J'annule les deux demandes de mon message précédent, @lenaic — inutile d'aller
lire le journal, le diagnostic existe. Le #148 l'a fait mieux que moi : ce
n'est pas
npm cimaisnpm audit, non borné,fetch-timeoutà 300 000 mset deux réessais. Les échecs s'y massent à 433–435 s.
Mon échec fait 437 s (7 min 17). Même signature, à la seconde près.
Deux conséquences pour cette demande :
#148 relève que
developest rouge sur ce même job (exécution #240, échec à433 s). L'arbre
services/dashboardde mon sommet est l'objet git identique àcelui de
develop, donc les deux branches échouent pour la même raison, aumême endroit ;
2 min 55, et il porte le ruff, le mypy strict, les 442 cas unitaires et les
deux paliers de couverture.
Je ne relance donc rien et je ne touche à rien de la chaîne : le #148 est à toi,
et une relance ne ferait que retirer un ticket à sa file. La relecture de cette
demande peut se faire sur le job Python.