Dette passe 1, lot L4 : mesures, alertes et déduplication #4
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "pr/dette-l4"
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?
Objet
Lot L4 de la passe 1 de dette : fenêtre d'epoch des trames, délais d'alerte calculés sur l'heure de réception du serveur, plafond d'insertions d'alertes, rechargement des règles d'alerte, déduplication des mesures par empreinte stockée et propriétaire configurable des boîtiers provisionnés.
Décisions de l'utilisateur à traiter dans le lot L4-bis
Ces deux points ont été tranchés après ce lot ; ils ne sont pas dans cette PR et seront livrés par le lot L4-bis, empilé après :
StoreOutcome::Inserted: une erreur de stockage rendDataError::Storesans alerte (aquaserveur/src/data_handler.rs).LEGIONELLA_COOLDOWNpar sonde ethardware_cooldown(list_id)par type de capteur (aquaserveur/src/alert_limits.rs), tous à 600 s.Dettes traitées et preuves
EPOCH_MAX= 2^31-1 (alert_boitiers.datetimereste unINT(11)), fenêtre de 366 jours en arrière et 300 s en avant autour de l'heure serveur,EpochError; le serveur refuse la trame avant tout stockageaquashared/src/frame.rs(validate_epoch,check_epoch_window),aquaserveur/src/data_handler.rsReceptionprise par le serveur, l'epoch de la trame ne sert plus à aucun délai ; délai par sonde, démarré après une insertion réussie ; table bornée (100 000 entrées) et purgéeaquaserveur/src/alert_limits.rs,data_handler.rs{id}_boitier_capteurs; délai matériel par type ; plafond de 10 insertions d'alertes par minute et par boîtier, partagé avec l'alerte 15 des accusés en erreuralert_limits.rs,data_handler.rs,config_ack_handler.rs,database/store.rscelsius_to_mptpar.round()(0.29 °C donnait 28) ; règles chargées au démarrage, rechargées sur changement de configuration et surfirstco, remplacées au lieu d'être ajoutéesdata_handler.rs,store.rs,alert_manager.rs,config_poller.rs,main.rsepoch_date BIGINT UNSIGNED NOT NULL, colonne générée stockéemesure_empreinte BINARY(32)(SHA-256 des valeurs,INVISIBLE),UNIQUE KEY uq_mesure_empreinte;store_framerendInsertedouDuplicate(code 1062), pas d'INSERT IGNORE; alertes seulement surInsertedaquaserveur/src/database/schema.rs,store.rs,data_handler.rsboitiers.user_idlu dansPROVISIONING_OWNER_USER_ID(défaut 1, valeur invalide : arrêt au démarrage)aquaserveur/src/provisioning/settings.rs,handler.rs,ENV_SETUP.md,.env.example, composesClé unique sur
epoch_dateseule écartée : des mesures différentes partagent leur epoch (5 184 lignes dans le dump).Tests et résultats
schema8 sur 9, déduplication 5 sur 5,alert_limits18 sur 18 ; contre-épreuves : délai global par boîtier, limite aux capteurs connus neutralisée,user_iden dur, chacune fait échouer son test.alert_limits.rs(13 tests),schema.rs,classify_insert, arrondi,measure_dedup.rs(5),measure_migration.rs,cooldown_bridge_integration.rsréécrit (dont une trame forgée à epoch futur qui ne fait pas taire l'alerte suivante),alert_rules_reload.rs, propriétaire configurable.test_parse_epoch_u32_maxdevienttest_parse_epoch_above_2038_rejected(comportement inversé volontairement) ; tests du délai sur l'epoch retirés et couverts parcooldown_bridge_integration.rs.cargo fmt --check,cargo clippy ... -D warnings: OK.cargo test --workspace: 288 passés, 0 échec, 57 ignorés (L3 : 242 et 45).Migrations et actions de déploiement
Migration
scripts/db/migrations/001_mesure_empreinte.sql(blocBEGIN NOT ATOMIC, idempotente), état en lecture seule par001_mesure_empreinte.check.sql, lancement parscripts/db/migrate-mesure-empreinte.sh <conteneur> [.env] [--check](mot de passe parMYSQL_PWD, jamais en argument). Pour chaque table%_boitier_datas:epoch_dateconverti enBIGINT UNSIGNED NOT NULL(arrêt si NULL ou négatif), colonne d'empreinte ajoutée, doublons exacts supprimés (plus petitidgardé), index unique ajouté.mariadb:10.6(prod en 10.6.22, essais faits en 10.11) ;%_boitier_datashors de l'hôte ;migrate-mesure-empreinte.sh ... --check, puis migration deux fois, puis--check(compteurs à 0) ;PROVISIONING_OWNER_USER_IDsi le défaut 1 ne convient pas, déployer, redémarrer.Choix faits
extd'abord.capteur_idrenseigné sur les alertes légionellose.mesure_empreinteINVISIBLEpour ne pas changer lesSELECT *des autres applications.boitiers.updated_at: une modification dealert_boitiersseule est prise en compte au rechargement suivant.GROUP BYsur l'empreinte).Hors périmètre
Boucle MQTT, accusé après écriture, session persistante, journalisation du payload (L5) ; limites et ACL du broker (L6) ; seuils légionellose par sonde (
fetch_legionella_thresholdslit une seule paire, à décider) ;alert_boitiers.datetimeenINT(11)(2038) ; documentation générale (L9).Retour arrière
%_boitier_datasprise avant la migration.Pile de PR
Base de cette PR :
pr/dette-l3. Fusion dans l'ordre de la pile ; apres chaque fusion,vigie.py retargetrebase la PR suivante surmain.pr/dette-l2: Dette passe 1, lot L2 : contrats partagés dans aquashared et client MQTT du boîtierpr/dette-l3: Dette passe 1, lot L3 : provisioning non destructif et identifiant validé à l'entrée MQTTpr/dette-l4: Dette passe 1, lot L4 : mesures, alertes et déduplication (cette PR)pr/dette-l5: Dette passe 1, lot L5 : boucle MQTT du serveur, livraison de la configuration et journauxpr/dette-l4bis: Dette passe 1, lot L4-bis : alertes même si l'écriture échoue, délai unique de 600 s, typage des capteurs, ordre des accusés MQTTpr/dette-l7: Dette passe 1, lot L7 : builds reproductibles, dépendances, durcissement des conteneurspr/dette-l6: Dette passe 1, lot L6 : authentification Mosquitto, ACL par compte, healthchecks réels, pile e2e isoléepr/dette-l8: Dette passe 1, lot L8 : tests non destructifs, fixtures synthétiques, script de tests sur base jetableCommits du lot
1ea0e37feat: bound frame epochs to a window around server time1fb65cafeat: deduplicate measures with a stored fingerprint2c0622afix: time alert delays on server reception and bound alert insertsef90407fix: reload alert rules on config change and stop caching threshold errors144f481feat: make the owner of provisioned boitiers configurable2886a12fix: show a current epoch in the server startup hintEach {id}_boitier_datas table gets an invisible stored generated column mesure_empreinte (SHA-256 of epoch_date and every sensor column, with COALESCE for NULLs) under a unique index. store_frame keeps a plain INSERT and reports Inserted or Duplicate from the unique violation (1062), so a QoS 1 resend or an EPIC-004 buffer replay is stored once and alerts only fire for a new measure. Duplicates are counted in the logs. Tables created by the server share one DDL (database::schema) with an unsigned BIGINT epoch, like the legacy tables. The idempotent migration scripts/db/migrations/001_mesure_empreinte.sql (launcher scripts/db/migrate-mesure-empreinte.sh, credentials from .env) converts signed epochs, adds the column, drops exact duplicates keeping the smallest id, then adds the index. A unique key on epoch_date alone is rejected: distinct measures share epochs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UH3JcaVWsmFLC6kyEsVL18Revue statique Vigie : PR #4
Tete
2886a1291a, basepr/dette-l3(3c8c48e78b), 6 commit(s), 33 fichier(s) ajoutes ou modifies.Statut
aquaprocess/revue-statique: failure (emoji dans le code ajoute)Constats : 0 bloquant, 8 majeur, 15 mineur, 2 style. Commentaires en ligne : 2 (mode
important, plafond 10).Verifications automatiques
Analyse qualitative
Verdict : aucun bloquant. Le lot est solide sur la fenêtre d'epoch, la déduplication par empreinte et la migration idempotente ; 3 constats majeurs (dont un écart connu renvoyé à L4-bis), 13 mineurs et 2 de style, tous liés à des lignes ajoutées par la plage 3c8c48e..2886a12.
Sécurité
BoitierId(numérique canonique) ouvalidate_boitier_id; toutes les valeurs sont liées parbind. Seule réserve :datas_table_ddlest publique et accepte une&strnon validée (aquaserveur/src/database/schema.rs:47).information_schemaet sont concaténés entre accents graves sans échappement dans lesEXECUTE IMMEDIATE(scripts/db/migrations/001_mesure_empreinte.sql:82). Risque faible (droit CREATE nécessaire), correction triviale.MYSQL_PWDsans valeur dans la ligne de commande, rien d'affiché. Le format de.envest lu de façon fragile (scripts/db/migrate-mesure-empreinte.sh:36).CooldownTableetInsertBudgetplafonnés à 100 000 entrées avec purge par minute ; les clés ne viennent que de boîtiers existants et de sondes limitées aux capteurs connus (data_handler.rs:252). Une trame de 2 000 valeurs ne produit pas plus d'alertes que de capteurs connus.Instantde réception). En revanche, le plafond par minute est partagé avec l'alerte 15 : un flot d'accusés en erreur peut affamer les alertes légionellose (aquaserveur/src/config_ack_handler.rs:127).Correction
alert_boitiers.datetimeenINT(11). Point de vigilance : une horloge RTC en avance de plus de 300 s rend le boîtier muet, mesures et alertes comprises (aquaserveur/src/data_handler.rs:234).INSERTsimple etclassify_insert, sansINSERT IGNORE. Toute violation d'unicité est toutefois classée en doublon, pas seulementuq_mesure_empreinte(aquaserveur/src/database/store.rs:55). L'empreinte ignoresw420(aquaserveur/src/data_handler.rs:248).store_temp_alertsn'est pas transactionnel ; un échec partiel laisse des alertes insérées sans délai (aquaserveur/src/database/store.rs:272).replace_rulescorrige le cumul aufirstco. Le déclenchement dépend du succès de la publication MQTT (aquaserveur/src/config_poller.rs:84), et un échec du chargement initial n'est pas retenté (aquaserveur/src/main.rs:108).id. La suppression est irréversible et ne regarde que les colonnescapteur_N_*(001_mesure_empreinte.sql:106) ; le--checkignore les tables sansepoch_date(001_mesure_empreinte.check.sql:46).unwrapniexpectajouté dans le code de production.Cohérence avec le compte rendu
Affirmations vérifiées à la tête
2886a12: constantes et fonctions de fenêtre dansaquashared/src/frame.rs:55,:62,:69,:77,:92,:105avec les assertions de compilation ;Reception, fenêtre avant stockage, doublon sans alerte, erreur de seuils non mise en cache dansdata_handler.rs;LEGIONELLA_COOLDOWN,hardware_cooldown, plafond,MAX_TRACKED_ENTRIES,admit_legionella,housekeepingdansalert_limits.rs;StoreOutcome,classify_insert,fetch_sensor_ids,store_frame, arrondi dansstore.rs; propriétaire (settings.rs,main.rs:51,handler.rs) ; règles (alert_manager.rs:189,:334,:343,config_poller.rs:82). Décomptes de tests conformes (13 dansalert_limits.rs, 6 dansschema.rs, 5 dansmeasure_dedup.rs, 6 danscooldown_bridge_integration.rs, 2 dansalert_rules_reload.rs).Écarts :
CooldownTableest cité enalert_limits.rs:93; la structure est en:73. Sans conséquence.process_data_frame(pipeline.rs:78, non modifiée, utilisée par les tests seulement) passe encoreframe.epochcomme horodatage.schema.rs:32) n'est garanti que par une comparaison de fragments (schema.rs:134).README.mdetTESTS.mddécrivent encorelast_alert_epochs(renvoyé à L9).Décisions renvoyées à L4-bis
aquaserveur/src/data_handler.rs:239, et:236pour la lecture des capteurs). Écart connu, validé par l'utilisateur, traité par L4-bis.AlertManager, tous à 600 s mais définis à plusieurs endroits (aquaserveur/src/alert_limits.rs:41,:32). Écart connu, validé, traité par L4-bis.data_handler.rs:234), qui coupe les alertes de la même façon.Tests
classify_insertavec une fausse erreur de base,limit_to_known_sensors,config_changes, réglages).SIGNALni sur plus de 9 capteurs, et exécutée sur toute la base de test (aquaserveur/tests/measure_migration.rs:58) ;startup_loads_the_rules_of_every_boitiern'affirme que « au moins un boîtier » ; aucun test de l'échec partiel destore_temp_alertsni de la famine des alertes légionellose par le plafond partagé.Questions ouvertes
%_boitier_datashéritées portent-elles d'autres colonnes queid,epoch_dateetcapteur_N_*? Si oui, la suppression des doublons est trop large.BEGIN NOT ATOMIC,EXECUTE IMMEDIATEet des colonnesINVISIBLE: hypothèse du compte rendu, à confirmer par la répétition prévue surmariadb:10.6.AlertManagerne sont que journalisées : est-ce voulu ?Autres constats (non publies en ligne)
aquaserveur/src/data_handler.rs:234InFutureau-delà de 300 s,TooOldau-delà de 366 jours) est refusée sans mesure ni alerte, avec un seul message d'erreur. Un boîtier dont l'horloge RTC avance de plus de 5 minutes devient donc muet, y compris pour une sonde à 120 °C. Piste : évaluer quand même les alertes sur l'heure de réception (rx.unix) quand l'epoch est hors fenêtre, ou insérer une alerte dédiée « horloge désynchronisée » soumise au plafond, et compter ces refus ; à rapprocher de la décision L4-bis sur les alertes indépendantes du stockage. (non publie en ligne : Forgejo l'ancrerait a la ligne 197 du commit2c0622a, decalee dans la vue de la PR)aquaserveur/src/data_handler.rs:239fetch_sensor_ids(ligne 236) ou deinsert_framerendDataError::Storeavant toute évaluation d'alerte. En3c8c48e,store_frameen erreur était journalisé et les alertes matériel et légionellose partaient quand même : c'est donc une régression du point de vue de l'alerte sanitaire. L4-bis doit faire partir les alertes même si l'écriture de la mesure échoue (seulDuplicatedoit les court-circuiter). (non publie en ligne : Forgejo l'ancrerait a la ligne 136 du commit1fb65ca, decalee dans la vue de la PR)aquaserveur/src/main.rs:107ef90407, decalee dans la vue de la PR)aquaserveur/src/main.rs:108ef90407, decalee dans la vue de la PR)aquaserveur/src/main.rs:152ef90407, decalee dans la vue de la PR)aquaserveur/src/main.rs:268ef90407, decalee dans la vue de la PR)aquaserveur/src/alert_limits.rs:41LEGIONELLA_COOLDOWNligne 32, par type avechardware_cooldowndont les deux bras dumatchsont identiques,AlertManager::new(600)dans main.rs) alors que la décision retenue est un délai unique de 600 s. Valeurs aujourd'hui toutes à 600 s, mais trois sources de vérité ; L4-bis doit les ramener à une constante unique.aquaserveur/src/alert_limits.rs:281no-unwrap-in-production(WARNING) :.unwrap()ou.expect()peut paniquer en production. 1 occurrence(s) sur lignes ajoutees (281), y compris d'eventuels modules de test internes.aquaserveur/src/config_poller.rs:84config_changesdépend delast_push, qui n'est mis à jour qu'après une publication MQTT réussie. Si la première publication d'un boîtier a échoué (absent delast_push), une modification ultérieure de sa configuration ne déclenche aucun rechargement des règles ; à l'inverse, une publication qui échoue en boucle provoque un rechargement à chaque passage. Piste : suivre les versions vues dans une table distincte de celle des versions publiées.aquaserveur/src/data_handler.rs:248sw420qui n'est pas stocké dans la table de mesures. Deux trames de même epoch et mêmes valeurs qui ne diffèrent que parsw420donneraient unDuplicateet l'alerte vibration de la seconde serait perdue. Cas probablement rare (même seconde), à documenter ou à couvrir en laissant passer l'alerte matériel sur un doublon dontsw420vaut 1.aquaserveur/src/data_handler.rs:306no-unwrap-in-production(WARNING) :.unwrap()ou.expect()peut paniquer en production. 2 occurrence(s) sur lignes ajoutees (306, 315), y compris d'eventuels modules de test internes.aquaserveur/src/database/schema.rs:47datas_table_ddlest publique et interpoleboitier: &strdans un nom de table sans validation ; la sûreté repose sur la documentation de l'appelant. Piste : prendre unBoitierId, ou appelervalidate_boitier_iddans la fonction (les tests qui utilisentl4nulletl4ddlrestent compatibles avec la liste blanche).aquaserveur/src/database/schema.rs:134expr_matches_the_migration_scriptne vérifie que des fragments de chaîne ; il ne prouve pas l'affirmation de la ligne 32 (« identique, au caractère près ») ni l'ordre numériquecapteur_10aprèscapteur_2. Piste : test sur base comparantGENERATION_EXPRESSION(information_schema.COLUMNS) d'une table migrée et d'une table créée pardatas_table_ddl, avec au moins 10 capteurs.aquaserveur/src/database/store.rs:55classify_inserttraite toute violation d'unicité (1062) comme un doublon, quelle que soit la clé. Sur une table existante (legacy) qui porterait une autre clé unique, une mesure réellement nouvelle serait écartée commeDuplicate, sans alerte et sans erreur. Piste : ne classer enDuplicateque si le message de l'erreur citeuq_mesure_empreinte(MESURE_EMPREINTE_INDEX), sinonInsertFailed.aquaserveur/src/database/store.rs:272store_temp_alertsinsère une ligne par sonde hors transaction et sort au premier échec par?: les sondes déjà insérées restent en base, mais data_handler.rs n'appellelegionella_insertedque surOk, donc leur délai ne démarre pas et elles sont réinsérées à la trame suivante (doublons dansalert_boitiers, bornés par le plafond). Piste : transaction commestore_hw_alerts, ou retour de la liste des sondes effectivement insérées.aquaserveur/src/main.rs:108load_all_alert_rulesau démarrage est seulement journalisé : les règles en mémoire restent vides jusqu'à une modification de fiche ou unfirstcode chaque boîtier, sans nouvelle tentative. Piste : réessayer au premier passage du poller, ou arrêter le serveur comme pourPROVISIONING_OWNER_USER_ID.aquaserveur/tests/measure_migration.rs:58SIGNALsur un epoch NULL ou négatif, ni l'ordre des colonnes au-delà de 9 capteurs ; le compte rendu indique que ces cas n'ont été vérifiés qu'à la main sur tables synthétiques. Le test exécute aussi la migration sur toutes les tables%_boitier_datasde la base de test (doublons des fixtures supprimés). Piste : ajouter une table à 10 capteurs et une table à epoch négatif dont on attend l'erreur.scripts/db/migrate-mesure-empreinte.sh:36env_valueprend la valeur brute après le premier=: guillemets, retour chariot (fichier CRLF) ou préfixeexportne sont pas gérés, si bien qu'unDB_PASSWORD="..."transmettrait les guillemets. Sans risque de fuite, mais source d'échec d'authentification en production. Piste : retirer\ret les guillemets englobants, ou documenter le format attendu.scripts/db/migrations/001_mesure_empreinte.check.sql:46epoch_date: la somme sur zéro ligne vaut 0 et la table n'est comptée nulle part, alors que la migration s'arrête dessus parSIGNAL(001_mesure_empreinte.sql:79). Le--checkpréalable peut donc annoncer un état sain et la migration échouer en cours de route. Piste : compter aussi les tables sansepoch_date.scripts/db/migrations/001_mesure_empreinte.sql:82t, lu dans information_schema, est concaténé entre accents graves sans échappement dans quatreEXECUTE IMMEDIATE(lignes 82, 89, 109, 116). Une table dont le nom contiendrait un accent grave permettrait d'injecter du SQL exécuté avec les droits ALTER et DELETE du compte de migration. Exploitation peu probable (droit CREATE nécessaire), mais défense simple : refuser parSIGNALtout nom qui ne respecte pas^[0-9A-Za-z_]+$, ou doubler les accents graves avecREPLACE.scripts/db/migrations/001_mesure_empreinte.sql:106epoch_dateet les colonnescapteur_N_id/data; toute autre colonne d'une table existante est ignorée. Si une table legacy porte d'autres colonnes (horodatage,moy, etc.), l'étape 3 supprime définitivement des lignes qui ne diffèrent que par ces colonnes. Piste : faireSIGNAL(ou au moins lister) les tables qui ont des colonnes horsid,epoch_date,capteur_N_*,mesure_empreinteavant toute suppression.aquaserveur/src/alert_limits.rs:22CooldownTable::startretire l'échéance la plus proche (min_by_keysuruntil, ligne 100). Équivalent tant que la période est la même pour toutes les clés ; aligner la phrase sur le code (comme le fait déjà le compte rendu).aquaserveur/src/main.rs:52Limites
Revue publiee en
COMMENT: l'auteur des PR et le relecteur sont le meme compte, Forgejo refuse APPROVE et REQUEST_CHANGES. Le blocage s'exprime par le statut de commitaquaprocess/revue-statique. Analyse statique et tests automatises seulement, sans fusion ni deploiement.@ -120,6 +124,14 @@ pub async fn handle_config_ack(// pour que l'opérateur soit notifié via le panneau d'alertes.let value = ack.error_detail.as_deref().unwrap_or("unknown");if !limits.admit_insert(boitier_id, Instant::now()) {[majeur] Le plafond de 10 insertions par minute est partagé entre l'alerte 15 (accusé
maj_boitier_ACKen erreur), les alertes matériel et les alertes légionellose. Un flot d'accusés en erreur publiés sur le topic du boîtier consomme tout le budget de la minute, etadmit_legionella(alert_limits.rs:221) refuse alors les alertes sanitaires ; comme leur délai ne démarre pas, elles sont retentées puis refusées à chaque trame tant que le flot dure. Piste : budget séparé par famille, ou part réservée (priorité) aux alertes légionellose.Source : revue.
@ -47,0 +49,4 @@// Propriétaire des boîtiers provisionnés par firstco (N34) : refus de// démarrer sur une valeur invalide plutôt qu'un rattachement silencieux.let provisioning = ProvisioningSettings::from_env().unwrap_or_else(|e| {eprintln!("❌ {} — arrêt.", e);[majeur] Caractere emoji U+274C dans une ligne ajoutee (regle IA-23).
Source : emoji.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.