Aller au contenu

Registre des drifts

Cette page catalogue les drifts du dépôt. Un drift est un écart entre le comportement attendu et le comportement observé, révélé à l’exécution — lors d’un run end-to-end (un test complet du système dans des conditions réalistes, sur le banc) — et que le lint et les tests unitaires ne voyaient pas. On y consigne aussi les pièges de revue : des bugs subtils identifiés par revue de code (souvent adversariale), et non au run.

Pourquoi un registre ? Ces écarts sont résolus au fil de l’eau ; sans trace, leur leçon se perd. Le registre capitalise cette mémoire institutionnelle : chaque entrée est citable par son identifiant (Dnn) et garde le symptôme, la cause et le correctif. La première salve provient du pipeline DataOps (ingestion OpenAlex par Dagster).

Le registre est une source de vérité YAML machine-lisible (docs/src/content/drifts/registre-drifts.yaml), validée au build par un schéma de types et rendue ci-dessous en tableau. Une entrée est ajoutée dans la même pull request que le correctif du drift. Le cadre et le format sont actés par l’ADR 0056.

Chaque entrée porte une portée (code : défaut du livrable, vaut pour la production ; env : artefact d’un banc précis ; harnais : outillage de test), une nature (drift e2e ou piège de revue) et un statut (corrigé, ouvert ou caduc).

23 entrées (0 ouverte).

Code (vaut pour la production) — 19

id nature symptôme cause correctif statut
D1 drift e2e Au premier run Dagster sur le banc, le K8sRunLauncher échoue : « Value in Mapping mismatches expected type for key dagster/image … Got None ». La code-location ne déclarait pas la variable d'environnement DAGSTER_CURRENT_IMAGE ; le run-launcher ne sait alors pas quelle image lancer pour les pods de run. Invisible aux tests unitaires (qui ne lancent pas de run K8s). DAGSTER_CURRENT_IMAGE = image de la code-location, déclarée dans le Deployment de la code-location. corrigé
D2 drift e2e Le bootstrap du watermark ne se déclenchait jamais : « rclone cat » d'un objet inexistant renvoie un code de retour 0 (pas d'erreur) avec une sortie standard vide. Comportement non-évident de l'outil : l'absence d'objet n'est pas un code d'erreur. Le code se fiait au code de retour. Révélé en testant contre un MinIO local. read_watermark traite une sortie vide ou un JSON invalide comme None (premier run), au lieu de se fier au code de retour. corrigé
D3 drift e2e « rclone lsf --recursive » sur data/works (≈ 250 M fichiers) bloque le run des minutes/heures ; le test passait, le run était inexploitable. Listing récursif prohibitif sur une arborescence massive. L'écart n'apparaît qu'à l'échelle réelle de la source, jamais sur les fixtures de test. _list_partitions liste par partition au premier niveau (--dirs-only, sans --recursive) ; le volume est borné par partition. corrigé
D5 drift e2e Le mécanisme merged_ids attendu sous s3://openalex/data/merged_ids/ est introuvable. merged_ids vit en réalité sous legacy-data/ : s3://openalex/legacy-data/merged_ids/<entité>/YYYY-MM-DD.csv.gz (colonnes merge_date,id,merge_into_id). L'ADR 0054 supposait data/merged_ids/. Hypothèse fausse sur une source externe, révélée en sondant la vraie source. Chemin corrigé vers legacy-data/merged_ids/ ; ADR 0054 mis à jour. corrigé
D7 piège de revue Perte silencieuse de fichiers merged_ids : quand max_merged_files tronque un lot, le watermark dépasse les fichiers non copiés, qui ne sont alors jamais rapatriés. Un watermark unique partagé entre les partitions et les merged_ids. Avancé sur la date des partitions, il « saute » des merged_ids non encore copiés. Trouvé par revue adversariale (42 findings, 16 confirmés), pas par les tests. Watermark merged_ids indépendant (clé merged_ids:<entité>), qui n'avance qu'aux fichiers réellement copiés. corrigé
D8 piège de revue Le filtre incrémental par updated_date est cassé : toutes les partitions passent le filtre. Comparaison de « updated_date=2019-07-01 » à une date nue « 2020-01-01 » : toujours vraie, car 'u' > '2' en ASCII. Trouvé en débogage. _partition_date extrait et valide la date (YYYY-MM-DD) avant toute comparaison. corrigé
D9 drift e2e Dagster ne résout pas le type de Config d'un asset (« Unable to resolve config type »). « from __future__ import annotations » (PEP 563) transforme les annotations en chaînes ; Dagster ne peut plus introspecter le type de config à l'exécution. Révélé au chargement de la code-location, pas au lint. Retrait de « from __future__ import annotations » des modules d'assets Dagster. corrigé
D10 piège de revue La résolution des dépendances échoue : « no version of dagster-dbt==1.13.7 ». Contrairement à dagster (1.13.7), la librairie dagster-dbt suit une numérotation propre 0.YY.Z. La correspondance est : dagster-dbt 0.YY.Z épingle dagster==1.(YY-14).Z. Pour dagster 1.13.7 il faut donc dagster-dbt==0.29.7 (et non 1.13.7). Découvert en sondant la résolution uv avant de coder l'étape 3.1. Épingler dagster-dbt==0.29.7 (résout dagster==1.13.7). Les paquets de l'écosystème Dagster en 0.YY suivent cette règle (à vérifier au prochain bump de dagster). corrigé
D11 piège de revue Une colonne fantôme « updated_date » (type DATE) apparaît dans les modèles staging alors qu'elle n'existe pas dans le JSON brut. read_json_auto active le partitionnement Hive par défaut : la date du chemin (updated_date=YYYY-MM-DD/) est injectée comme colonne. Un SELECT * la propagerait. Révélé en sondant le schéma inféré sur les fixtures. hive_partitioning=false sur les sources works/authors + projection EXPLICITE des colonnes en staging (jamais SELECT *). La période curated vient des vars Dagster, pas du chemin brut. corrigé
D12 piège de revue Un glob « merged_ids/works/*.csv.gz » raterait le fichier réellement présent. Le fichier merged_ids sur disque s'appelle « 2022-07-15.gz » (CSV gzippé sans infixe .csv), pas « 2022-07-15.csv.gz ». Hypothèse de nommage fausse, vérifiée sur le fixture. Glob « merged_ids/works/*.gz » (pas « *.csv.gz ») + lecture forcée read_csv_auto. corrigé
D13 drift e2e Le Parquet curated est introuvable au glob « run=<id>/**/*.parquet » après un « dbt build » pourtant réussi. dbt-duckdb interprète une « location » external terminée par « / » comme une CLÉ d'objet unique (l'objet écrit s'appelle littéralement « run=<id>/ »), pas comme un dossier de fichiers .parquet. Révélé au premier dbt build réel contre MinIO : build vert, relecture vide. Nommer explicitement le fichier dans la location (« …/run=<id>/part.parquet ») ; le préfixe dt=…/run=…/ reste l'unité immuable relisable via « run=<id>/*.parquet ». corrigé
D14 drift e2e La CI « DataOps (Python) » échoue (3 tests) alors que tout passe en local : « dbt parse » s'arrête sur « Env var required but not provided: AWS_ACCESS_KEY_ID ». Le « dbt parse » paresseux (ensure_manifest, quand le manifest absent — target/ est git-ignoré) doit RENDRE profiles.yml, qui lit le secret S3 via env_var() SANS défaut. En CI (aucune clé AWS, pas de manifest pré-généré) le rendu échoue, bien que le parse ne fasse aucune I/O S3. Masqué en local par un manifest déjà généré. Révélé au 1er run CI. Injecter des identifiants S3 FACTICES (os.environ.setdefault — jamais d'écrasement d'un vrai accès) avant le parse paresseux, parité avec le Dockerfile. Le « dbt build » runtime garde le vrai environnement (chemin séparé). Le parse devient hermétique partout. corrigé
D15 piège de revue « rclone hashsum sha256 » sur un objet S3 échoue : « hash unsupported: hash type not supported » (code 1) — impossible de calculer le sha256 des parts pour le manifest. S3/MinIO n'expose pas de sha256 côté serveur (seulement l'ETag/MD5) ; rclone réclame un hash serveur par défaut. Révélé en sondant rclone contre un MinIO épinglé avant de coder. Forcer le calcul côté client avec « --download » : rclone télécharge les octets et hache localement (« rclone hashsum sha256 --download »), résultat identique à hashlib/shasum. Un test unitaire vérifie que la commande hashsum porte « --download » (anti-régression). corrigé
D17 drift e2e Le pod de RUN (K8sRunLauncher) crashe au démarrage : « Couldn't import module dagster_postgres.run_storage » — avant même d'exécuter l'asset. Le run worker EST l'image code-location (lancée depuis DAGSTER_CURRENT_IMAGE) : il doit rehydrater l'instance Dagster du cluster (dagster.yaml → PostgresRunStorage + K8sRunLauncher). Or le pyproject excluait volontairement dagster-postgres et dagster-k8s (« deps de l'orchestrateur seulement ») — hypothèse fausse pour ce modèle d'exécution. Invisible en tests (pas de run K8s). Ajout de dagster-postgres==0.29.7 + dagster-k8s==0.29.7 (numérotation 0.YY.Z comme dagster-dbt, ↔ dagster 1.13.7) aux dépendances de la code-location. psycopg2-binary (LGPL) entre en transitif, assumé côté dataops (Python, hors gate licences Node, ADR 0055). corrigé
D18 drift e2e Le check GE bloquant ge_raw_contract échoue : DuckDB lève « Invalid Input Error: Map keys must be unique » alors que l'ingestion a réussi. check_raw lit le brut OpenAlex en SELECT * : read_json_auto infère le schéma nested COMPLET, dont abstract_inverted_index (objet JSON à clés de mots) converti en MAP ; certains enregistrements réels ont des clés dupliquées → DuckDB rejette. Invisible en tests hermétiques (fixtures propres) ; révélé sur le snapshot réel. Projeter dans check_raw les SEULES colonnes que les suites GE valident (works : id/referenced_works/authorships ; authors : id) au lieu de SELECT * — on ne parse plus les champs non validés. Plus robuste et plus léger. corrigé
D19 drift e2e Run SUCCESS mais AUCUN lineage dans Marquez (pas de namespace citation ni de job raw_snapshot) et aucun run MLflow : émission silencieusement en no-op. Piège ADR 0086 (contrat cluster) : OPENLINEAGE_URL / MLFLOW_TRACKING_URI posés sur le Deployment de la code-location gRPC NE se propagent PAS aux pods de run du K8sRunLauncher. _emit_lineage est gardé par « if OPENLINEAGE_URL » → no-op ; le logging MLflow du drift idem. La garde validate.sh vérifie la variable dans le MANIFESTE (Deployment), pas dans la config de run → ne détecte pas le trou. Injecter OPENLINEAGE_* + MLFLOW_TRACKING_URI au niveau RUN via container_config.env (tag dagster-k8s/config), pour ingestion_job ET transform_job ; test anti-régression. GOTCHA : après rebuild de la code-location, redémarrer daemon ET webserver (cache gRPC) pour que les nouveaux tags atteignent les runs. corrigé
D20 drift e2e index_load échoue : « relation "researchers" does not exist » — sur un déploiement neuf, le pipeline ne peut pas charger l'index. L'asset index_load CONSOMME le schéma de l'index sans le créer (frontière explicitée dans son docstring + ADR 0058 §4.1 « migrations versionnées »). Or RIEN dans le dépôt ne fournissait ce schéma : la migration supposée appliquée au déploiement n'existait pas. Migration versionnée deploy/migrations/0001_researchers_index.sql : table researchers (embedding vector(384) + index HNSW cosinus, fts tsvector + index GIN, index de partition dt/run), conforme ADR 0058 §4.1/4.2, idempotente. Le branchement d'exécution reste au déployeur (frontière infra) ; le dépôt fournit le SQL. corrigé
D21 drift e2e Le check GE bloquant ge_marts_collab échoue (viole le contrat not_null) alors que le résultat correct est un mart VIDE : le mart contient une ligne à author_a/author_b NULL. Sur un jeu sans arête auteur→auteur (les œuvres citées n'ont pas d'auteur co-ingéré → relation vide), la matérialisation « external » de dbt-duckdb écrit un placeholder de schéma à toutes colonnes NULL. Un garde côté MODÈLE (WHERE author_a IS NOT NULL) est inopérant : la ligne ne vient pas du SELECT (qui renvoie 0 ligne) mais de la matérialisation. Filtrer la ligne fantôme au POINT DE LECTURE : check_marts lit WHERE author_a IS NOT NULL → un mart sans paire est un état valide (0 paire réelle). Idem _count_rows du manifest : NOT (COLUMNS(*) IS NULL), agnostique du mart, pour que row_count ne compte pas le placeholder (sinon row_count=1 ≠ sha256 des octets → contrat manifest cassé). corrigé
D23 drift e2e transform_job échoue aux tests dbt relationships : 903 authorships.author_id et 7658 marts_researchers.author_id pointent vers des auteurs absents de la table authors (clés étrangères pendantes). raw_snapshot ingère works et authors par FICHIERS, partition par date (ADR 0054). Sur un petit banc, les tranches sont DISJOINTES : les works (p. ex. 2016) citent ~856 auteurs précis, quasi absents de l'échantillon d'authors (p. ex. 2026). Structurel à l'échantillonnage borné par fichiers (OpenAlex partitionne par updated_date, pas par contenu) ; en prod (snapshot complet) la cohérence est native. Mode « échantillon cohérent » opt-in dans raw_snapshot (config coherent_sample, OFF par défaut, ADR 0063) : après le sync des works, dérive une tranche authors contenant EXACTEMENT les auteurs cités, depuis les objets author inline (id/display_name/orcid) de works.authorships, écrite sous la partition réservée updated_date=coherent-sample. Vérifié au banc : 0 FK manquante, dbt PASS=69, transform_job RUN_SUCCESS sans seed manuel. corrigé

Environnement (artefact d'un banc) — 2

id nature symptôme cause correctif statut
D4 drift e2e Un « rclone copy » d'un fichier de 915 Ko qui prend 1,2 s en local prend plusieurs minutes dans un pod de run sur le banc. Cause réelle (diagnostic cluster #268) : SeaweedFS épuisait ses slots de volume (-volume.max=0 → plafonné à 13, tous saturés → « Not enough data nodes found ») ; toute écriture d'une nouvelle collection restait bloquée en retries TCP. L'hypothèse initiale (pods BestEffort throttlés) était FAUSSE : l'egress était bon, le throttling n'existait pas. Corrigé côté cluster (#268 : -volume.max=100 + volumeSizeLimitMB=1024) — upload S3 revenu à 1 s. Décision atlas : borner quand même les runs dbt par prudence (DuckDB gourmand en RAM) via dagster-k8s/config — choix de prudence, pas correctif de throttling. corrigé
D6 drift e2e L'Application affiche « Synced/Healthy » mais les ressources attendues sont absentes du cluster. L'Application réconcilie path:"." en mode NON récursif : les manifestes rangés dans un sous-dossier sont ignorés. Révélé au déploiement (sync vert, ressources manquantes). Manifestes placés à la racine du dépôt Gitea réconcilié. corrigé

Harnais (outillage de test) — 2

id nature symptôme cause correctif statut
D16 drift e2e La CI « DataOps (Python) » échoue : « FileNotFoundError: 'rclone' » sur le test d'intégration du manifest, alors qu'il passe en local. rclone N'EST PAS une dépendance pip : il vit dans l'IMAGE (Dockerfile), pas sur l'hôte de CI ni la machine d'un contributeur. Le test d'intégration du manifest shelle rclone directement sur l'hôte (pas dans un conteneur, contrairement à `mc`). Masqué en local (rclone installé). Révélé au 1er run CI de l'étape 3.4. Auto-saut du test si rclone est absent (requires_rclone(), comme le skip Docker du fixture MinIO) ; la logique de l'asset reste couverte par les tests FakeRclone (rclone mocké). Garde-fou ADR 0057 : un test exigeant un outil hôte non garanti est self-skipping. corrigé
D22 drift e2e Le pre-push échoue pour tout le monde au hook check-audit (audit:security). Override de sécurité undici périmé dans pnpm-workspace.yaml (>=7.25.0) : de nouvelles advisories (GHSA-vmh5-mc38-953g, vxpw-j846-p89q, hm92-r4w5-c3mj + une moderate) exigent >=7.28.0 (bypass TLS, DoS WebSocket, routage cross-origin). undici est transitif via jsdom et @effect/platform-node. Un seuil d'override périmé casse check-audit (dette racine). Relever l'override undici à « >=7.28.0 <8 » + pnpm install. audit:security repasse vert. corrigé