[EF-12] Agrégation et zone or matérialisée #35
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
2 participants
Notifications
Due date
Blocks
Depends on
#38 [EF-10] API REST et mandataire
g2/enervision
#39 [EF-09] Recommandations : trois règles versionnées
g2/enervision
Reference
g2/enervision#35
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Exigence couverte
EF-12, ENF-04, ENF-07
Épreuve servie
EC05 · Data, ETL et BI
Charge estimée
2,5 j.h
Ce qu'on veut obtenir
Matérialiser la zone or dans PostgreSQL et TimescaleDB, seule couche que l'API et Grafana liront, pour tenir la cible de latence sans interroger MinIO en ligne.
Critères d'acceptation
Comment on le vérifie
Tests tests/integration/gold/
Commande pytest tests/integration/gold, puis la mesure de latence sous charge légère
Preuve le chiffre de latence au 95e centile et la trace de remontée d'un échantillon
Hors périmètre
Les écrans qui consomment ces données. Le moteur de recommandations.
État : la chaîne de la zone or est écrite et testée, la demande de fusion #124 est ouverte, deux critères attendent la base et le #34
Un service
services/etl/et une migration0016. Brancheolivier/35-etl-zone-or, chaîne d'intégration verte sur les sept tâches.Quatre entrées, qui suivent l'ordonnancement du §13 de
docs/data/etl-pipeline.md:python -m etl.agregation --jour AAAA-MM-JJqualite_jourcomprisepython -m etl.chargement --jour AAAA-MM-JJpublic.mesureetpublic.qualite_jour, en upsertpython -m etl.export --jour AAAA-MM-JJ --vers DOSSIER --controlerpython -m etl.lignage --jour AAAA-MM-JJ --taille 20Les critères, un par un
La zone or est écrite dans les hypertables, alimentée depuis la zone argent. Le code est écrit et testé, mais sur des fixtures de zone argent : le #34 n'a pas démarré. Le job lit le schéma du §10, qui est figé, donc il tournera sur une zone argent réelle sans qu'une ligne change ici. Le chargement en base est couvert par huit cas d'intégration qui attendent une base joignable — voir ci-dessous.
Une série de 24 heures se lit sous 400 ms au 95e centile. Mesuré au #29 à 4,09 ms au p95 sur 352 807 lignes, marge d'un facteur 100. Le cas qui refait la mesure sur la zone or chargée est écrit (
test_une_fenetre_de_24_heures_se_lit_sous_400_ms_au_95e_centile, vingt exécutions, relevé écrit dans un fichier) et se saute faute de base. À rejouer et coller ici.Un export agrégé pour le reporting est produit, reconstituable à la mesure près. Trois choses le rendent reconstituable, et aucune n'est une affirmation de document : les compteurs
releves_pris_en_compteetreleves_exclusvoyagent sur chaque ligne — sans eux personne ne peut refaire la moyenne ; la règle d'agrégation est écrite dans un manifeste JSON à côté du CSV, avec son empreinte SHA-256 ; etreconstituer()refait le calcul depuis la zone or et compare ligne à ligne. Deux cas vérifient qu'il échoue — un chiffre retouché à la main, une ligne disparue — parce qu'un contrôle qui ne peut pas échouer ne prouve rien.Un contrôle sur échantillon tiré au hasard remonte de la valeur or jusqu'à la lecture bronze d'origine.
etl.lignage, avec les deux formes d'objet bronze : un objet par minute pour/current, un objet comprimé par fenêtre pour/readings(#107) — un contrôle qui ne saurait remonter que le premier laisserait tout l'historique rattrapé hors de portée. Le cas nominal qui compte le plus est celui de la ligne imputée : brute nulle en or, valeur nulle en bronze, et concordance — parce que ce qui est comparé est la valeur brute et non la valeur retenue. Comparer la retenue rendrait toute imputation suspecte.Ce que la mise en place a révélé, et qui vaut plus que le ticket
1. Le §12 et la base ne parlaient pas de la même chose. Le document décrivait la zone or comme trois tables —
mesure_horaire,qualite_jour,charge_site. En base il y en a sept, dontmesureau pas de la minute, et nimesure_horairenicharge_siten'existaient : « la zone or est écrite dans les hypertables » n'avait pas de cible. La migration0016les pose,mesure_horaireen agrégat continu TimescaleDB etcharge_siteen vue. L'alternative — deux tables chargées par le job — est écartée en tête du fichier : un agrégat rechargé en même temps que la série peut la contredire le temps d'une transaction, et il faut écrire le code qui le rejoue. Le choix est réversible : les deux objets se suppriment sans rien perdre,public.mesurerestant la seule source. C'est ce qui a permis de trancher sans attendre la réunion. @marvin, c'est le #38 qui les lira : dis en relecture si la forme te va.2. La zone or reste au pas de la minute, et c'est délibéré. 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, sans table de correspondance, qui rend le contrôle par échantillon possible — et c'est le critère 4 de ce ticket.3. Le chargement refuse les jours déjà comprimés.
mesureest comprimée au delà de sept jours (#29). Unon conflict do updatequi 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 à décomprimer pour 5,4 Mo comprimés, relevé surenervision_preprodle 3 septembre. Recharger un vieux jour après une correction de règle d'imputation reste possible, avec--forcer, et le message porte la commande de décompression : c'est un geste délibéré, pas un effet de bord d'un job de cron qui tourne toutes les heures.4. Trois pièges attrapés en écrivant, qu'aucune relecture de code n'aurait vus.
SET TimeZone = 'UTC'est posé dans la connexion, et tout le médaillon est en UTC (ADR 0005).pytzest une dépendance que rien n'importe. DuckDB en a besoin pour rendre unTIMESTAMPTZen objet Python : sans lui, « Required module 'pytz' failed to import » tombe aufetchalldu 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.clé=valeur, et ajoutedomain,tableetdtaux colonnes du fichier. Le chargement nomme donc ses colonnes une par une plutôt que de faire confiance à unSELECT *.5. Le job échoue plutôt que d'écrire une journée douteuse. Au delà de 30 % de trous (garde-fou du §11), sur une valeur d'énumération que la base refuserait — attrapée ici et non dix minutes plus tard sur une ligne parmi dix mille — et sur plus de relevés disponibles que la cadence n'en permet, qui n'est pas une donnée mais un défaut de déduplication de la zone argent : charger quand même donnerait un taux de disponibilité au dessus de 100 % et des agrégats calculés sur des doublons.
Les preuves
tests/integration/gold/test_chaine_zone_or.pyjoue la chaîne entière sur une journée de 1 440 minutes pour deux sites — agrégation, journal de qualité, export horaire, reconstitution, remontée de vingt lignes tirées au hasard — sans MinIO, sans base et sans réseau : les fixtures de zone argent sont construites en Parquet local,.gitignoreexcluant*.parquet.Ce qui reste, et pourquoi le ticket n'est pas fermé
0008–0016ne sont pas appliquées enenervision_prod, c'est le lot de la #114. Tant qu'elles ne le sont pas, les huit cas d'intégration base se sautent et les deux preuves manquantes ne peuvent pas être produites.app: #113.Documentation
docs/data/etl-pipeline.md§12 réécrit sur ce qui est posé, §2 remis à l'état réel, et le point ouvert « chargement gold → PostgreSQL non spécifié » du §14 est refermé ;docs/POSTGRESQL.md: les deux objets dérivés passés au même crible d'exposition Grafana, table par table, que les sept du #105 ;db/migrations/README.md: le motif de l'agrégat continu, leWITH NO DATAet pourquoi unWITH DATAmatérialiserait 352 807 lignes au démarrage du serveur ;services/etl/README.md: les entrées, les codes de sortie que lit cron, et les deux pièges ;tests/integration/README.md: ce que chaque test d'intégration exige, et le fait qu'un test sauté ne prouve rien.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.lenaic referenced this issue2026-09-04 13:43:09 +00:00