Purge des états : un bug de 2 ans corrigé + nettoyage automatique + page Tâches enrichie (PRs #2650, #2651, #2652)

En testant la nouvelle vue Activité de la 4.82, on est tombé sur quelque chose d’inattendu — et ça a donné trois PRs qui se complètent. Merci énorme à @Will_71 pour ses tests sur sa propre base ! :folded_hands:

Épisode 1 — le bug invisible depuis 2 ans (PR #2650)

En voulant purger les états d’un capteur bavard (décocher « Conserver l’historique »), la tâche répondait instantanément : 0 states to delete… alors que la vue Activité affichait des milliers d’états pour cette feature.

Verdict après archéologie git :

Question Réponse
Cassé depuis v4.45.0 (août 2024)
Cause La migration DuckDB (#2104) a déplacé les états, mais 2 requêtes SQLite ont été oubliées
Endroit 1 purgeStatesByFeatureId (le toggle « Conserver l’historique ») comptait et supprimait dans SQLite, désormais vide → no-op total
Endroit 2 Le garde-fou de device.destroy (« too much states ») comptait aussi SQLite → protection anti-blocage morte
Pourquoi invisible Rien n’affichait l’historique par feature… jusqu’à la vue Activité

La #2650 migre les deux vers DuckDB (même requête que celle que destroy utilisait déjà) et garde le nettoyage des restes SQLite pour les installations pas encore migrées.

Épisode 2 — combien d’états orphelins traînent chez vous ? (PR #2651)

Conséquence du bug : chaque device/feature supprimé depuis 2 ans a pu laisser ses états « orphelins » dans DuckDB — invisibles, mais scannés par chaque requête et occupant du disque.

Sur proposition de @pierre-gilles : une tâche automatique one-shot au démarrage (pattern de la migration DuckDB : Variable système posée uniquement en fin de run → si Gladys redémarre en plein milieu, elle repart seule au boot suivant — testé en vrai en coupant le container au milieu :scissors:).

Et « la plus lente possible » : pas de comptage préalable (qui tenait la connexion lecture 15-20 min sur ma base), parcours en tranches hebdomadaires avec une pause de 5× la durée de chaque tranche — la purge ne consomme jamais qu’une fraction des ressources et s’adapte toute seule à la machine.

Test terrain Base Orphelins trouvés Durée Impact ressenti
Moi (serveur partagé avec HA + 2ᵉ Gladys) 448 M d’états 20 :grinning_face_with_smiling_eyes: 1 h 49 (cache froid) / 29 min (chaud) Vue Activité : 125 ms pendant la purge (contre 44 s avec la 1ʳᵉ version non bridée)
@Will_71 (laptop Fedora) 228 M d’états 45 400 000 (~20 % de sa base !) ~29 min « Aucun ralentissement, peut-être +2 s sur l’Activité »

Les 45 M de Will confirment que ce nettoyage automatique était nécessaire pour tout le monde.


Avant/Après :

Encore un grand merci à @Will_71 pour les tests complets des 2 PR

Épisode 3 — la page Tâches qui aurait détecté le bug en un jour (PR #2652)

Si la page Tâches avait affiché « 0 état trouvé » face à une Activité pleine, le bug de purge n’aurait pas survécu 2 ans. C’est maintenant le cas — les tâches portent des données structurées (traduites côté front, donc dans toutes les langues) :

  • Cible : Détecteur de présence cuisine › Détection mouvement
  • Étapes en direct : En attente de la base de données… / Calcul du nombre d’états… / Suppression des états… / Suppression des agrégats… (deux purges simultanées affichent honnêtement l’une qui travaille et l’autre qui attend)
  • Comptages : États à supprimer : N DuckDB + N SQLite — N agrégats, puis États supprimés : … en compte-rendu persistant
  • Compteur live pour la purge d’orphelins, % par tranches, durée qui tique pour les tâches en cours

Conception validée par les échanges sur la #2528 : aucune modification du wrapper de jobs — les tâches restent lancées par le pattern événement existant, et chaque job attache lui-même ses données via un paramètre optionnel d’updateProgress.

Pour la review

@pierre-gilles les 3 PRs sont prêtes (tests + coverage 100 % sur les lignes modifiées, CI verte, validées en réel sur deux installations). Ordre de merge : #2650#2651#2652 (la dernière est empilée sur les deux autres, son diff rétrécira après leurs merges).

Quand même cool quand un développement met à jour un bug caché :grin:

Trop cool @Terdious merci pour les développements et merci @Will_71 pour les tests !! :smiley:

Je vais regarder !

@Terdious Tu pourrais lancer Fable 5 sur les commentaires de CodeRabbit ?

Il y a des trucs vraiment critiques :smiley:

C’est fait !

CodeRabbit avait raison sur le point critique : la dernière tranche non bornée évaluait les états écrits pendant la purge contre un instantané figé des features — un device appairé pendant les ~30 min de purge pouvait perdre ses premiers états. Corrigé avec un cutoff capturé avant l’instantané, appliqué à toutes les tranches, + test de régression. Le test manquant de la branche « zéro feature » est ajouté aussi ; le warning SQL injection est un faux positif (placeholders uniquement), répondu dans la PR.

Mea culpa partagé avec Fable 5 sur ce coup : deux décisions raisonnables isolément (tranche finale ouverte pour couvrir les états écrits en cours de run + snapshot des features en début) devenues dangereuses combinées — et je n’avais pas relancé de passe de review dédiée en fin de développement auprès de Fable. Bonne illustration que la review croisée bot + agent + humain attrape des angles morts différents :grinning_face_with_smiling_eyes:

@pierre-gilles,

J’ai vu que tu avais mergé #2651, j’ai donc résolu les conflits sur la PR #2652

@Terdious merci pour la PR, c’est bien mais je trouve qu’on couple trop les données spécifiques de chaque type de job avec l’objet job générique.

Je propose une PR corrigée :

Dis-moi si ça te paraît bien :slight_smile:

@pierre-gilles,

J’ai tout lu, je suis totalement d’accord avec l’analyse et avec le correctif, c’est plus propre qu’un catalogue « four-tout ». Merci à toi !!

J’ai fais le test chez moi avant la release en prod (cet aprem) :

J’ai 40 millions d’états pourtant, mais je n’ai jamais du tomber dans le cas du bug ^^

Tant mieux finalement ^^
Idem avec 400 millions, j"en avais 20 seulement ^^ Mais je ne supprime jamais d’équipement / feature :wink: :sweat_smile:

Par contre @Will_71 en avait énormément !!