Panne de l'importation quotidienne et nettoyage des doublons
Billet #326 : Panne silencieuse de l'importation quotidienne et doublons d'historique
Type : Débogage / Correction / Intégrité des données
Composants concernés : run_pipeline.sh, scripts/catchup_prices.py, base de données (table historique), tests/test_pipeline.py, tests/schema.sql
1. Contexte et Symptômes
Un constat simple a déclenché ce débogage de l'application : cela faisait quelques semaines que j'avais noté que l'historique des prix en base de données s'était arrêté; or l'application elle-même semblait fonctionner normalement (conteneurs actifs, aucun crash).
2. Processus d'Investigation et Constatations
Connexion au serveur et lecture des journaux
Une connexion SSH au VPS de production a permis d'inspecter la tâche planifiée (crontab -l) et le journal /var/log/cron-pipeline.log. L'horaire du cron était correct (02h00 UTC, du lundi au samedi), mais chaque exécution échouait depuis un certain temps avec le même message :
Constatation 1 — Le lanceur quotidien avait disparu du serveur
Le fichier run_pipeline.sh, responsable de déclencher l'importation dans le conteneur applicatif, n'existait tout simplement plus sur le VPS et n'avait jamais été suivi dans Git. La dernière exécution réussie datait du 2 juillet 2026 — exactement la date où l'historique s'arrêtait en base. L'application, la base de données et Docker fonctionnaient normalement ; seul le déclencheur automatique manquait.
Constatation 2 — Un rattrapage manuel a révélé un second problème : des doublons
Après restauration du lanceur, un script de rattrapage (scripts/catchup_prices.py) a été utilisé pour combler les journées de bourse manquantes entre le 3 juillet et le 14 août. Une fois les données rafraîchies dans l'interface, mes tests ont revélés que chaque titre affichait deux lignes identiques par date (exemple observé : Air Canada TSE:AC, prix dupliqué du 1er juillet au 14 août).
Constatation 3 — La cause racine des doublons : une contrainte manquante en production
L'analyse du schéma réel de la table historique en production a révélé l'absence d'une contrainte UNIQUE (titre_id, date_releve). Le code d'écriture (insert_data()) repose sur une logique ON DUPLICATE KEY UPDATE, en supposant l'existence de cette contrainte — qui n'existait que dans le schéma de test (tests/schema.sql), pas en production. Résultat : chaque nouvelle tentative d'écriture pour une date déjà présente créait une ligne supplémentaire au lieu de la mettre à jour.
Bilan chiffré des doublons : 3 896 lignes en double au total — 3 891 générées après le 2 juillet 2026 (conséquence directe de la panne et du rattrapage), et 5 doublons préexistants depuis novembre 2025 avec des valeurs légèrement différentes.
3. Causes Racines Identifiées
| # | Cause | Impact | Statut |
|---|---|---|---|
| 1 | run_pipeline.sh absent du serveur et jamais versionné dans Git |
Arrêt complet de l'importation automatique du 3 juillet au 17 août 2026 | Corrigé |
| 2 | Absence de contrainte UNIQUE (titre_id, date_releve) en production |
Chaque réécriture d'une date existante créait un doublon au lieu d'une mise à jour | Corrigé |
| 3 | Schéma de test et schéma de production désynchronisés | Le bogue était invisible en test, la contrainte n'existant que côté test | Corrigé |
4. Solutions Implantées
4.1 — Restauration et fiabilisation du lanceur automatique
Recréation de run_pipeline.sh, déploiement sur le VPS et validation d'une exécution complète de bout en bout (cron → conteneur → base de données). Le script est désormais conservé dans le dépôt Git pour éviter toute nouvelle disparition.
4.2 — Rattrapage des données manquantes
Création de scripts/catchup_prices.py, un outil réutilisable et idempotent (modes --dry-run et --verifier) qui réutilise l'enrichissement Marketstack existant pour combler les journées de bourse absentes, sans dupliquer les dates déjà présentes.
4.3 — Nettoyage des doublons et verrouillage définitif
Sauvegarde de sécurité de la table (historique_backup_20260817), suppression des 3 896 lignes en double (conservation de la ligne la plus ancienne), puis ajout d'une contrainte UNIQUE (titre_id, date_releve) directement en production. Cette contrainte rend la classe de bogue structurellement impossible, indépendamment de toute future erreur applicative. Le script de nettoyage, à usage unique, a été retiré du dépôt une fois sa mission terminée.
4.4 — Renforcement de la suite de tests
Ajout de tests verrouillant explicitement ce qui a fait défaut :
- tc-pipe-integrity01 — le schéma refuse un second prix pour le même titre à la même date.
- tc-pipe-integrity02 — une réimportation de la même date met à jour la ligne existante sans créer de doublon.
- tc-pipe-fx01 — la conversion USD → CAD reste correcte, sans effet de bord du correctif.
- tc-pipe-date04, tc-pipe-catchup01, tc-pipe-catchup02 — blocage du dimanche et calendrier de rattrapage.
Le schéma de test (tests/schema.sql) a été aligné sur la contrainte désormais posée en production.
5. Vérification et Résultat
- Suite de tests complète : 251/251 tests passent.
- Couverture des cas de test pipeline : 75 % → 86 % (30/40 → 36/42).
- Couverture globale documentée : 82 % → 86 % (90/110 → 96/112).
- Base de données : une seule ligne par titre et par date, historique à jour jusqu'au 14 août 2026, aucune ligne perdue lors du nettoyage (sauvegarde conservée).
- Importation automatique : exécution quotidienne restaurée et validée sur le serveur.
6. Bilan de Consommation token
925,67 unités consommées pour un coût de 9,25 $. Ce chiffre couvre l'ensemble de la session de débogage — connexions SSH répétées, diagnostics de base de données, rattrapage des données et renforcement de la suite de tests. Considérant que ça m'a pris environ 3 heures à investiguer, réparer, améliorer et documenter cet incident en utilisant l'IA, ce coût token est insignifiant comparativement au coût d'un développeur sur la même durée de travail.