[infra] La chaîne ETL passe aux dix minutes, l'agrégat continu suit #246

Closed
opened 2026-09-09 13:14:18 +00:00 by olivier · 1 comment
Member

Exigence couverte

EF-11 · Tableau de bord, vue du parc. Sert aussi la réserve écrite d'EF-07 (âge du seau horaire) et l'ENF-16 (le manuel dit la cadence réelle).

Épreuve servie

EC05 · Data, ETL et BI

Charge estimée

une demi-journée

Ce qu'on veut obtenir

Le pavé « état des sites » annonce un dernier relevé par site. Sur la machine, ce relevé a entre 28 et 87 minutes d'âge, parce que la crontab de deploy porte encore la chaîne horaire (:00 / :17 / :27) : la passe argent ne couvre les minutes de l'heure H qu'à (H+1):00, et le chargement ne les écrit en base qu'à (H+1):27.

Le #205 a corrigé ce problème pour la prévision en ramenant la chaîne au quart d'heure — 9 à 23 minutes d'âge. Il n'a pas été déployé, et il ne visait pas la série au pas de la minute, qui est ce que l'écran affiche : dashboard/repository.py lit public.mesure par jointure latérale sur le dernier relevé de chaque site, jamais mesure_horaire.

On veut ramener la chaîne aux dix minutes, et l'agrégat continu avec elle. L'âge de la dernière minute en base tombe alors à 6–15 minutes, et le seau horaire devient lisible à (H+1):10.

Ce que ce ticket ne prétend pas faire. Ce n'est pas du temps réel, et le mot ne sera pas employé dans le dépôt : c'est une chaîne par lots dont le lot fait dix minutes. Dix minutes est aussi le plancher de la forme actuelle : la passe argent relit la journée entière à chaque tour (elle ne traite pas l'incrément), elle coûte environ une minute en fin de journée, et six passages par heure portent la lecture de bronze à six minutes par heure. Descendre à cinq minutes demande de rendre cette passe incrémentale, et de le mesurer d'abord.

Les deux réglages ne peuvent pas être posés séparément, et c'est la leçon du #205 : un seau se matérialise quand il tient entièrement dans [now - start_offset, now - end_offset]. Rafraîchir plus vite que la chaîne ne charge, c'est matérialiser un seau nourri de lignes pas encore chargées ; plus lentement, c'est laisser l'agrégat derrière la série qu'il résume.

Critères d'acceptation

  • Les trois passes ETL sont déclarées aux dix minutes dans taches_planifiees, régulières, dans l'ordre argent → or → chargement, et les trois tiennent dans une même tranche de dix minutes.
  • end_offset et schedule_interval de public.mesure_horaire valent dix minutes, posés par une migration qui n'appelle pas refresh_continuous_aggregate (il refuse de tourner dans une transaction) et qui écrit son sens de retour.
  • Le dernier chargement d'une heure laisse au moins cinq minutes avant le passage de la politique qui matérialise le seau de cette heure.
  • tests/ci/test-fraicheur-chaine.sh lit la cadence dans taches_planifiees et y confronte la politique de la dernière migration qui en pose une : changer l'une des deux moitiés sans l'autre sort rouge, sans qu'on ait à retoucher le banc.
  • La borne d'attente du verrou de la zone or (ENERVISION_ETL_VERROU_ATTENTE) ne dépasse plus la cadence : elle valait 900 s pour une chaîne horaire et n'avait pas suivi le #205.
  • Les trois minutes qui séparent l'argent de l'or ne sont pas réduites — c'est la marge la plus serrée de la chaîne, et la seule que le verrou ne couvre pas.
  • La prévision reste horaire (EF-07 en demande une par heure, pas six) et garde au moins cinq minutes de marge derrière un chargement.

Comment on le vérifie

bash tests/ci/test-fraicheur-chaine.sh
cd infra/ansible && yamllint . && ansible-lint
ansible-playbook ../../tests/ci/test-role-app.yml
shellcheck services/etl/bin/*.sh tests/ci/*.sh
pytest tests/unit -q

Et sur le serveur, après ansible-playbook site.yml --tags app :

sudo -u deploy crontab -l                      # six passes par heure, pas une
grep -c "chargé" /var/log/enervision/gold.log  # ~144 par jour, contre ~24
sudo docker exec -e PGPORT=5433 -u postgres ev-postgres psql -U postgres \
  -d enervision_prod -tAc \
  "select round(extract(epoch from (now() - max(horodatage)))/60) from public.mesure;"

La dernière commande est la preuve qui compte : elle doit rendre un nombre entre 6 et 15.

Manuel d'exploitation à mettre à jour

docs/runbooks/etl.md — tableau des minutes, écart entre la cadence déclarée et la crontab de la machine, section « Rafraîchir l'agrégat continu », compte de lignes attendu dans gold.log. Et docs/data/etl-pipeline.md §13 pour l'ordonnancement.

Risque et retour arrière

Le risque principal est la marge argent → or, ramenée de trois minutes sur une passe qui en coûte une en fin de journée : la passe de 00:00, qui clôt la veille, est la plus lourde du jour (1 440 minutes × 7 sites). Si elle déborde, l'or lit une partition à moitié réécrite, son contrôle d'indicateur refuse la journée (code 4), et la passe suivante rattrape dix minutes plus tard. Le mode de panne est donc bruyant et borné, pas silencieux.

Le second est l'ordre de déploiement : la crontab d'abord, la politique ensuite. La migration passe au démarrage de l'API, donc les deux voyagent dans le même lot.

Retour arrière : la section en fin de la migration ramène la politique au quart d'heure, et taches_planifiees les minutes du #205. Les deux se défont ensemble, sans quoi le banc de fraîcheur refuse le dépôt.

### Exigence couverte EF-11 · Tableau de bord, vue du parc. Sert aussi la réserve écrite d'EF-07 (âge du seau horaire) et l'ENF-16 (le manuel dit la cadence réelle). ### Épreuve servie EC05 · Data, ETL et BI ### Charge estimée une demi-journée ### Ce qu'on veut obtenir Le pavé « état des sites » annonce un `dernier relevé` par site. Sur la machine, ce relevé a **entre 28 et 87 minutes d'âge**, parce que la crontab de `deploy` porte encore la chaîne horaire (`:00` / `:17` / `:27`) : la passe argent ne couvre les minutes de l'heure `H` qu'à `(H+1):00`, et le chargement ne les écrit en base qu'à `(H+1):27`. Le #205 a corrigé ce problème **pour la prévision** en ramenant la chaîne au quart d'heure — 9 à 23 minutes d'âge. Il n'a pas été déployé, et il ne visait pas la série au pas de la minute, qui est ce que l'écran affiche : `dashboard/repository.py` lit `public.mesure` par jointure latérale sur le dernier relevé de chaque site, jamais `mesure_horaire`. On veut ramener la chaîne **aux dix minutes**, et l'agrégat continu avec elle. L'âge de la dernière minute en base tombe alors à 6–15 minutes, et le seau horaire devient lisible à `(H+1):10`. **Ce que ce ticket ne prétend pas faire.** Ce n'est pas du temps réel, et le mot ne sera pas employé dans le dépôt : c'est une chaîne par lots dont le lot fait dix minutes. Dix minutes est aussi le **plancher** de la forme actuelle : la passe argent relit la journée entière à chaque tour (elle ne traite pas l'incrément), elle coûte environ une minute en fin de journée, et six passages par heure portent la lecture de bronze à six minutes par heure. Descendre à cinq minutes demande de rendre cette passe incrémentale, et de le mesurer d'abord. **Les deux réglages ne peuvent pas être posés séparément**, et c'est la leçon du #205 : un seau se matérialise quand il tient entièrement dans `[now - start_offset, now - end_offset]`. Rafraîchir plus vite que la chaîne ne charge, c'est matérialiser un seau nourri de lignes pas encore chargées ; plus lentement, c'est laisser l'agrégat derrière la série qu'il résume. ### Critères d'acceptation - [ ] Les trois passes ETL sont déclarées aux dix minutes dans `taches_planifiees`, régulières, dans l'ordre argent → or → chargement, et les trois tiennent dans une même tranche de dix minutes. - [ ] `end_offset` et `schedule_interval` de `public.mesure_horaire` valent dix minutes, posés par une migration qui n'appelle pas `refresh_continuous_aggregate` (il refuse de tourner dans une transaction) et qui écrit son sens de retour. - [ ] Le dernier chargement d'une heure laisse au moins cinq minutes avant le passage de la politique qui matérialise le seau de cette heure. - [ ] `tests/ci/test-fraicheur-chaine.sh` **lit** la cadence dans `taches_planifiees` et y confronte la politique de la dernière migration qui en pose une : changer l'une des deux moitiés sans l'autre sort rouge, sans qu'on ait à retoucher le banc. - [ ] La borne d'attente du verrou de la zone or (`ENERVISION_ETL_VERROU_ATTENTE`) ne dépasse plus la cadence : elle valait 900 s pour une chaîne horaire et n'avait pas suivi le #205. - [ ] Les trois minutes qui séparent l'argent de l'or ne sont pas réduites — c'est la marge la plus serrée de la chaîne, et la seule que le verrou ne couvre pas. - [ ] La prévision reste horaire (EF-07 en demande une par heure, pas six) et garde au moins cinq minutes de marge derrière un chargement. ### Comment on le vérifie ``` bash tests/ci/test-fraicheur-chaine.sh cd infra/ansible && yamllint . && ansible-lint ansible-playbook ../../tests/ci/test-role-app.yml shellcheck services/etl/bin/*.sh tests/ci/*.sh pytest tests/unit -q ``` Et sur le serveur, après `ansible-playbook site.yml --tags app` : ``` sudo -u deploy crontab -l # six passes par heure, pas une grep -c "chargé" /var/log/enervision/gold.log # ~144 par jour, contre ~24 sudo docker exec -e PGPORT=5433 -u postgres ev-postgres psql -U postgres \ -d enervision_prod -tAc \ "select round(extract(epoch from (now() - max(horodatage)))/60) from public.mesure;" ``` La dernière commande est la preuve qui compte : elle doit rendre un nombre entre 6 et 15. ### Manuel d'exploitation à mettre à jour `docs/runbooks/etl.md` — tableau des minutes, écart entre la cadence déclarée et la crontab de la machine, section « Rafraîchir l'agrégat continu », compte de lignes attendu dans `gold.log`. Et `docs/data/etl-pipeline.md` §13 pour l'ordonnancement. ### Risque et retour arrière **Le risque principal est la marge argent → or**, ramenée de trois minutes sur une passe qui en coûte une en fin de journée : la passe de 00:00, qui clôt la veille, est la plus lourde du jour (1 440 minutes × 7 sites). Si elle déborde, l'or lit une partition à moitié réécrite, son contrôle d'indicateur refuse la journée (code 4), et la passe suivante rattrape dix minutes plus tard. Le mode de panne est donc bruyant et borné, pas silencieux. **Le second est l'ordre de déploiement** : la crontab d'abord, la politique ensuite. La migration passe au démarrage de l'API, donc les deux voyagent dans le même lot. Retour arrière : la section en fin de la migration ramène la politique au quart d'heure, et `taches_planifiees` les minutes du #205. **Les deux se défont ensemble**, sans quoi le banc de fraîcheur refuse le dépôt.
Author
Member

La PR #248 porte ce ticket. Branche olivier/246-cadence-dix-minutes, rebasée sur develop (b5667d6).

Les sept critères d'acceptation sont tenus par tests/ci/test-fraicheur-chaine.sh, qui compte 19 contrôles verts. Le banc ne code plus la cadence en dur : il la lit dans taches_planifiees et y confronte la politique de la dernière migration qui en pose une, de sorte qu'une troisième cadence n'aura pas à le réécrire.

Un huitième point est apparu en route, et il n'était pas dans le ticket. Le #244, fusionné pendant l'écriture du lot, fait publier par chaque lanceur son intervalle de crontab (ev_ops_tache_cadence_secondes). Les trois de l'ETL déclaraient encore 900 s, et le rebasage est passé sans conflit — deux modifications du même fichier à des endroits différents. Laissé tel quel, le tableau de bord etl et l'alerte du #228 auraient toléré 45 minutes de chaîne arrêtée au lieu de 30, sans rien signaler. Corrigé, et le banc contrôle maintenant les sept tâches.

Ce qui reste à faire, et qui ne peut pas l'être depuis un poste : le relevé d'après déploiement. C'est la preuve de fermeture de ce ticket.

sudo -u deploy crontab -l                      # six passes par heure
grep -c "chargé" /var/log/enervision/gold.log  # ~144 par jour, contre ~24
sudo docker exec -e PGPORT=5433 -u postgres ev-postgres psql -U postgres \
  -d enervision_prod -tAc \
  "select round(extract(epoch from (now() - max(horodatage)))/60) from public.mesure;"

La dernière doit rendre un nombre entre 6 et 15. Rappel : la machine a deux cadences de retard, le quart d'heure du #205 n'ayant jamais été déployé — c'est --tags app qui rendra les 1 h → 10 min visibles, pas la fusion.

La PR #248 porte ce ticket. Branche `olivier/246-cadence-dix-minutes`, rebasée sur `develop` (`b5667d6`). **Les sept critères d'acceptation sont tenus par `tests/ci/test-fraicheur-chaine.sh`**, qui compte 19 contrôles verts. Le banc ne code plus la cadence en dur : il la lit dans `taches_planifiees` et y confronte la politique de la dernière migration qui en pose une, de sorte qu'une troisième cadence n'aura pas à le réécrire. **Un huitième point est apparu en route, et il n'était pas dans le ticket.** Le #244, fusionné pendant l'écriture du lot, fait publier par chaque lanceur son intervalle de crontab (`ev_ops_tache_cadence_secondes`). Les trois de l'ETL déclaraient encore `900` s, et le rebasage est passé **sans conflit** — deux modifications du même fichier à des endroits différents. Laissé tel quel, le tableau de bord `etl` et l'alerte du #228 auraient toléré 45 minutes de chaîne arrêtée au lieu de 30, sans rien signaler. Corrigé, et le banc contrôle maintenant les **sept** tâches. **Ce qui reste à faire, et qui ne peut pas l'être depuis un poste :** le relevé d'après déploiement. C'est la preuve de fermeture de ce ticket. ``` sudo -u deploy crontab -l # six passes par heure grep -c "chargé" /var/log/enervision/gold.log # ~144 par jour, contre ~24 sudo docker exec -e PGPORT=5433 -u postgres ev-postgres psql -U postgres \ -d enervision_prod -tAc \ "select round(extract(epoch from (now() - max(horodatage)))/60) from public.mesure;" ``` La dernière doit rendre un nombre **entre 6 et 15**. Rappel : la machine a deux cadences de retard, le quart d'heure du #205 n'ayant jamais été déployé — c'est `--tags app` qui rendra les 1 h → 10 min visibles, pas la fusion.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
g2/enervision#246
No description provided.