Plan dossier — CVN-N014-ED-S01 : Fondation — flip global du backend object-storage XCom (load-bearing)¶
Story: CVN-N014-ED-S01 — OP wp#242 · GH #1105 · parent Epic CVN-N014-ED (wp#241, GH #1104)
Type: implementation plan (ADR-68), submitted for committee plan_review (gate In specification → Specified).
Date: 2026-06-05
Status: branche feat/CVN-N014-ED-S01-xcom-objectstorage-foundation (depuis origin/main). Recon faite. Aucun code de flip écrit avant ce gate.
v4 — les deux faits-recon clés VÉRIFIÉS in-pod (pas assertés), + une correction de prémisse Epic. v1 : « subset-prod par-DAG » (mécaniquement impossible) → REJECTED (
1ffeb71e, Meeting #248). v2 : C = blue/green éphémère (= probe dynamique B2). v3 : la recon descend C → A (recon-fondé) ; Tier-1 = contingence. v4 : application de la rigueur §G.3 aux deux faits qui justifient A (avant ils étaient lus du fichier, pas vérifiés). Vérifié in-pod (airflow config get-value, scheduler réelairflow-scheduler-…-jsmhp) : (1)enable_xcom_pickling=Falseeffectif (l'env ne l'override pas) ✅ — clé de voûte de A tient ; (2) source deXComObjectStorageBackend.serialize_value(common-io 1.4.2) lue : sous seuil = son proprejson.dumps(value, XComEncoder)(octets identiques à BaseXCom pickling-off → kill-switch sub-seuil sûr) ; au-dessus = DB stocke la string de chemin → kill-switch mid-run casse l'aval → revert + re-trigger (§H). NOUVEAU FINDING (vérifié, casse une prémisse Epic) :XComEncoderne sérialise PAS numpy (TypeError: Object of type ndarray is not JSON serializable, testé sur la forme littérale de s43) → l'Epic §2 « sérialise nativement numpy » est faux à 1.4.2 ; lejson.dumpstournant avant le seuil, numpy échoue à toute taille → route X n'est PAS drop-in pour s43 (§S02). A tient pour S01 (le parc existant est pur-JSON, round-trip vert) ; le numpy est le problème de S02.
Partie I — Dossier (problématisation → Definition of Done)¶
Chapitre 1 — Problématisation (sans jargon)¶
Le problème, en une phrase : dans nos chaînes de traitement Airflow, chaque étape doit parfois passer un résultat à l'étape suivante — et aujourd'hui, chaque étape bricole sa propre façon de le faire, ce qui a déjà cassé une chaîne en silence.
Le contexte. Un DAG Airflow est une suite de tâches qui s'exécutent l'une après l'autre, chacune dans son propre conteneur (un « pod » jetable). Quand une tâche produit une donnée dont la suivante a besoin, il faut la lui transmettre. Airflow offre pour cela un petit casier de messagerie interne appelé XCom : une tâche y dépose une valeur, la suivante va la relire. C'est conçu pour de petits messages — un nombre, un drapeau, un court dictionnaire de métriques. Ce casier vit dans la base de données d'Airflow, et il est petit : au-delà d'environ 48 Ko, la valeur est tronquée silencieusement (on ne voit pas l'erreur).
Ce qui a mal tourné. Pour transmettre une donnée volumineuse (un tableau de prédictions), une tâche ne peut pas utiliser le petit casier. Le développeur a donc improvisé : écrire le tableau dans un fichier sur le disque local du pod, puis passer juste le chemin du fichier par le casier. Problème : le disque local d'un pod n'est pas partagé avec les autres pods. Chaque pod a écrit dans son disque ; le pod qui devait relire a regardé son disque à lui — vide. Résultat : un diagnostic qui rendait « indéterminé » à tous les coups, sans la moindre erreur visible. Ni les tests (qui tournaient sur une seule machine où le disque était partagé), ni la relecture humaine (l'info « le disque n'est pas partagé » n'apparaît nulle part dans le code) ne pouvaient l'attraper.
La cause profonde n'est pas « le mauvais dossier a été choisi ». C'est qu'il n'existe aucune règle commune sur la façon de transporter une donnée entre tâches : chacun réinvente un stockage intermédiaire, avec une cible qui peut être fausse et que personne ne vérifie. Tant que la règle n'existe pas, le même bug peut resurgir ailleurs.
La direction de la solution. Plutôt que de corriger ce cas précis, on installe une règle unique pour tous : Airflow sait nativement, via un backend de stockage dédié, envoyer automatiquement les grosses valeurs vers le stockage objet partagé du projet (S3, déjà utilisé partout) et ne garder qu'un pointeur dans la base — le tout piloté par un seuil de taille. Les petits messages restent où ils sont (dans la base) ; seules les grosses valeurs partent vers S3. Le développeur écrit juste « renvoie cette valeur » ; le transport devient une affaire d'infrastructure, plus de code bricolé par tâche. Cette Story pose cette fondation : activer ce backend globalement, après avoir prouvé que ça ne casse rien d'existant.
Ce que cette Story ne fait PAS : elle ne déploie pas encore le basculement en production (c'est une étape séparée, sous garde-fous), et elle ne résout pas le cas des tableaux numériques (numpy) — qui, on le verra, demande une stratégie propre (Story suivante, S02).
Chapitre 2 — User stories mises en œuvre¶
| # | En tant que… | je veux… | afin de… |
|---|---|---|---|
| US-1 | auteur de DAG | transmettre une donnée entre tâches avec un simple return / xcom_pull, sans coder de stockage intermédiaire |
ne plus jamais introduire la classe de bug « cible de stockage fausse » (incident s43) |
| US-2 | opérateur | un standard unique et documenté du transport inter-tâches (+ un ADR) | savoir, sans deviner, comment les données circulent et où elles sont stockées |
| US-3 | opérateur | un pre-flight qui prouve in-pod que le backend est correctement configuré avant tout basculement | ne pas activer un flip silencieusement inerte (variable d'env perdue) ou cassé (connexion S3 absente) |
| US-4 | opérateur / DRI | un kill-switch documenté et répété (revert + re-trigger) | revenir en arrière en sécurité si le flip pose problème, sans corrompre les runs en cours |
| US-5 | mainteneur | un audit outillé (statique + round-trip dynamique) du parc XCom | mesurer le blast-radius du flip et détecter tout type non-sérialisable avant qu'il ne casse |
| US-6 | quant / data-scientist | savoir explicitement que numpy n'est pas transporté nativement | choisir la bonne stratégie (pass-by-référence) pour les payloads ML, sans surprise en production |
Chapter 3 — Hypotheses (tested in this Story)¶
All hypotheses below were verified in-pod against the real scheduler (not asserted from the config file) — that verification is the method.
| # | Hypothesis | How it was tested | Result |
|---|---|---|---|
| H1 | The current XCom universe is JSON-only and enumerable (so a flip cannot break serialization for the existing parc). | airflow config get-value core enable_xcom_pickling / donot_pickle in the scheduler pod; static audit of the DAG tree (xcom_serialization_audit.py). |
Confirmed — enable_xcom_pickling=False effective; audit: 0 numpy producer, 116 @task dicts. |
| H2 | Below the threshold, the object-storage backend is byte-equivalent to BaseXCom (so the flip is near-inert today). |
Read XComObjectStorageBackend.serialize_value source (common-io 1.4.2); compared the sub-threshold path to BaseXCom. |
Confirmed — sub-threshold returns the same json.dumps(value, XComEncoder) bytes. |
| H3 | The backend transports JSON-serializable values only — numpy is NOT native. | Executed the backend's exact call json.dumps(value, XComEncoder) in-pod on a bare ndarray, a unicode ndarray, and s43's literal dict-of-arrays. |
Confirmed (refutes the Epic premise) — TypeError at any size; the json.dumps precedes the threshold check. |
| H4 | A bare backend revert is unsafe once values are offloaded (the kill-switch needs more than a config revert). | Read serialize_value/deserialize_value: above threshold the DB stores only the object-store path string. |
Confirmed — a mid-run revert hands downstream the path string → kill-switch = revert + re-trigger. |
| H5 | No real XCom traffic crosses the threshold today (so the offload path has no production exposure yet). | Static audit (only array payload = s43, which writes to base_dir, not XCom) + s43 on hold + threshold=-1 effective. |
Confirmed — the flip's only at-risk code path is currently un-exercised; first real user is s43 under S02. |
Chapter 4 — State of the art¶
XCom and the small-message contract. Airflow's XCom is the canonical mechanism for passing small data between tasks; the official Best Practices explicitly warn against pushing large objects through it and recommend storing large data in an external store (S3/HDFS/…) and passing a reference through XCom (the pass-by-reference / intermediary storage pattern — Astronomer, Passing data between Airflow tasks). The metadata-DB XCom is bounded by the DB column type (≈ 48 KB on Postgres before truncation), which makes large-payload XCom an anti-pattern rather than a configuration choice.
Custom XCom backends and object storage. Airflow supports pluggable XCom backends (AIRFLOW__CORE__XCOM_BACKEND). The apache-airflow-providers-common-io provider ships XComObjectStorageBackend, which transparently offloads values above xcom_objectstorage_threshold to an object store (xcom_objectstorage_path) while keeping small values inline in the DB — the threshold-driven pass-by-reference pattern as a centralized policy rather than per-DAG code (Airflow Object Storage + common.io provider docs). Crucially, the value is still serialized through Airflow's XComEncoder (JSON-by-default; pickling gated by enable_xcom_pickling), so the backend transports JSON-serializable values — not arbitrary binary objects. numpy/pandas require either a registered serializer (Airflow's airflow.serialization serde framework) or explicit pass-by-reference.
Shared-filesystem alternative (rejected). A shared RWX filesystem (e.g. Scaleway File Storage CSI) would give drop-in POSIX semantics, but at the cost of a mutable shared mount (concurrency, single-point-of-failure, manual GC) — against the project's stateless-pods architecture. Object storage is the project's existing substrate (MLflow artifacts, logs) and the idiomatic choice for stateless workers.
References. Airflow Best Practices — passing data between tasks; Astronomer Pass data between tasks / Custom XCom backends; Apache Airflow Object Storage + apache-airflow-providers-common-io provider documentation; Airflow airflow.serialization serde framework (numpy/pandas serializers).
Chapter 5 — Definition of Done¶
- Serialization audit published — static (
xcom_serialization_audit.py, run ondags/: 129 sites, 0 numpy_suspect, 116 unknown) + dynamic in-pod round-trip (xcom_roundtrip_probe.py: control-flow GREEN, numpy EXPECTED-FAIL). - Hardened pre-flight (
xcom_flip_preflight.py) proves in-pod the provider + S3 connection + effective dotted-section config; read-only by default. - Foundation ADR (ADR-0100) merged + live on docs.cvntrade.eu, with the 5 invariants.
- Kill-switch runbook (revert + re-trigger + mandatory rehearsal gate) merged + live.
- Story canonical docs — plan (this dossier), architecture, test strategy, hub, MLOps readiness.
- Tests — 19 unit tests green;
make test-unitfast tier;mkdocs build --strictgreen. - Committee — plan_review PASSED (
8d7efca0, 0 blocker) + pr_review PASSED (3a73b7e1, 0 blocker). - Epic §2 corrected (numpy not native).
- Prod flip (
values-prod.yaml) — out of scope / staged, gated on a kill-switch rehearsal (separate change, operator go). - numpy strategy (S02) — out of scope, deferred (default route Y).
Partie II — Plan technique (decision record + design)¶
§0. Decision record — pourquoi A (recon-fondé), pas C¶
§0 v2 liait correctement B1 (rollout) à B2 (audit) : la réponse rollout est fonction de la complétude atteignable de l'audit. v3 tire la recon jusqu'au bout, et elle rend la complétude atteignable → A suffit, C est prématuré.
Règle (inchangée) : B2 prouvablement complet → A suffit ; sinon → C (instance éphémère qui exerce le swap hors-prod). v3 montre que la recon rend B2 énumérable et testable sans instance parallèle (§0bis), donc on prend A, et on ne bâtit C que si l'audit dément la recon.
Décision (opérateur, 2026-06-05) + résultats v4 (round-trips déjà exécutés in-pod) :
1. Audit statique + round-trips ciblés D'ABORD : faits en partie. JSON inline = vert (parc existant couvert) ; forme s43 numpy = rouge (le backend ne sérialise pas numpy, §0bis.3).
2. Pour S01 → A : le parc existant étant pur-JSON (vérifié), B2 est complet pour ce que S01 flippe → A : flip near-inert + kill-switch armé, pas de Tier-1. L'échec numpy n'est pas une régression de S01 (s43 n'envoie rien en XCom aujourd'hui).
3. L'échec numpy → finding S02 (route X non drop-in, §S02), pas une contingence Tier-1 : il est déterministe et déjà caractérisé, rien à chasser dans une instance parallèle.
4. Kill-switch obligatoire dans tous les cas (revert AIRFLOW__CORE__XCOM_BACKEND → BaseXCom + re-trigger des runs offloadés in-flight, §H).
§0bis. La recon empties C — le détail¶
Deux faits vérifiés in-pod (scheduler réel airflow-scheduler-…-jsmhp, via airflow config get-value — pas lus du fichier), et ce que le plan doit en tirer :
enable_xcom_pickling=False+donot_pickle=True— effectifs in-pod (airflow config get-value core enable_xcom_pickling→False; l'env ne l'override PAS versTrue). → univers XCom actuel JSON-only, énumérable. (C'était la clé de voûte de A ; elle est maintenant prouvée, pas assertée.)- Seul payload large = s43
np.savez(src/commun/finetune/diagnostic/hamilton/s43_io.py:46) — et s43 écrit le npz versbase_dir(store partagé), PAS vers XCom (« XCom carries only KEYS + a compact summary — the arrays NEVER do », s43_io.py:5). De plus s43 est en hold. → aucun trafic XCom> seuilne circule aujourd'hui, et même un s43 actif (architecture actuelle) n'en produirait pas. - NOUVEAU —
XComObjectStorageBackendne transporte PAS numpy (vérifié in-pod, common-io 1.4.2) :serialize_valuefaitjson.dumps(value, cls=XComEncoder)avant le test de seuil, etXComEncoderlèveTypeError: Object of type ndarray is not JSON serializablesur un ndarray nu, un ndarray unicode, et la forme littérale de s43 (dict imbriqué{family: {crypto: (y_va, [P_i])}}). Le contrôle-flow plain-JSON, lui, round-trip vert. → l'Epic §2 (« sérialise nativement numpy ») est faux à 1.4.2 ; route X n'est pas drop-in (§S02).
Conséquences (que v2 ratait) :
- Le flip est quasi-inerte pour l'existant. Tout est JSON et sous-seuil → XComObjectStorageBackend délègue à BaseXCom (stockage DB inline, octet pour octet). Le « swap de sérialisation global-instantané » que §0 v2 traitait comme LE risque ne touche, pour l'existant, que le chemin inline-JSON — le plus bénin.
- Le vrai risque — la serde d'offload (> seuil) — n'a AUCUN trafic réel. Donc C (« rejouer les vrais DAGs ») valide l'inline, PAS la serde d'offload : aucun payload prod ne franchit le seuil, le code neuf n'est jamais exercé par le replay. La convergence « C = B2 » de v2 ne tient que pour l'inline ; pour la partie qui compte, C est aveugle. L'exercer exige d'injecter la forme numpy s43 — ce qui est un round-trip ciblé, pas « rejouer les vrais DAGs ».
- Donc C est sur-construit (instance éphémère) ET mal visé (chemin bénin). Right-sized = round-trip ciblé sur la forme s43, sans instance parallèle.
- Même si le comité veut C (défense en profondeur post-rejet, c'est son droit) : C tel que spécifié valide le chemin bénin ; même C doit injecter le numpy s43 pour valoir quelque chose.
Problème¶
Story load-bearing de l'Epic CVN-N014-ED. AIRFLOW__CORE__XCOM_BACKEND est instance-global : flip = tous les DAGs d'un coup. Mais (cf. §0bis) le flip est aujourd'hui quasi-inerte : l'incrémentalité de l'offload vient du seuil, et l'offload n'a aucun trafic. Le seul risque non-bénin (serde d'offload) est testable ciblé, son premier usage réel sera s43 (route X, §S02).
État courant (recon) :
- Airflow 2.10.4 / python 3.12 (airflow_docker/Dockerfile.k8s:20).
- Backend = défaut BaseXCom (docker-compose.yaml:43,173), object-storage désactivé (xcom_objectstorage_threshold=-1).
- enable_xcom_pickling=False + donot_pickle=True (airflow.cfg:152,212) → XCom JSON-only.
- Section [common.io] présente → provider apache-airflow-providers-common-io a priori bundlé (2.10.x) → prouvé dur par le pre-flight (§G).
Approche¶
A. Le flip (config-only, via Helm — #378)¶
Backend cible : airflow.providers.common.io.xcom.backend.XComObjectStorageBackend. 4 env vars dans infra/helm/airflow/values-prod.yaml (Helm SSoT, ConfigMap cvntrade-env-config) :
| Variable | Valeur | Rôle |
|---|---|---|
AIRFLOW__CORE__XCOM_BACKEND |
airflow.providers.common.io.xcom.backend.XComObjectStorageBackend |
bascule (global) |
AIRFLOW__COMMON_IO__XCOM_OBJECTSTORAGE_PATH |
s3://<conn_id>@<bucket>/xcom |
préfixe d'offload |
AIRFLOW__COMMON_IO__XCOM_OBJECTSTORAGE_THRESHOLD |
<calibré> (cf. C) |
sous → DB ; au-dessus → S3 |
AIRFLOW__COMMON_IO__XCOM_OBJECTSTORAGE_COMPRESSION |
` (ougz`) |
compression optionnelle |
Comportement réel (source 1.4.2 lue in-pod) : serialize_value fait s_val = json.dumps(value, cls=XComEncoder) inconditionnellement, puis teste le seuil. Sous seuil → renvoie s_val (octets identiques à BaseXCom pickling-off → DB inline, lisible par BaseXCom ⇒ kill-switch sub-seuil sûr). Au-dessus → écrit le fichier object-store + stocke BaseXCom.serialize_value(str(p)) = la string de chemin en DB (⇒ kill-switch mid-run dangereux, §H). Aujourd'hui : tout sous seuil → near-inert. (Note : ce n'est pas un super()/délégation — le backend re-sérialise lui-même ; l'identité d'octets sous-seuil tient parce que les deux utilisent XComEncoder.)
B. Audit = statique + round-trips ciblés (B2 right-sized, sans instance)¶
- Énumération statique :
scripts/xcom_serialization_audit.py(read-only) — par DAG, deux dépôts (dags/+cvntrade-airflow-dags), recense les types XCom (return @task,xcom_push,.output,xcom_pull) et classe JSON / numpy / pandas / custom. Recon : surface modeste (48 réfs, 7xcom_push; majorité launchers KPO) ; seul non-JSON = s43. - Round-trips ciblés (la garantie, sans instance parallèle) — partiellement DÉJÀ exécutés in-pod (v4) :
- Inline JSON ✅ :
json.dumps({...}, XComEncoder)→json.loads(.., XComDecoder)sur un dict de contrôle-flow = vert (round-trip identitaire). Le parc existant (JSON-only) est couvert. - Offload / forme s43 ❌ : la forme littérale de s43 (
{family: {crypto: (y_va: ndarray, [P_i: ndarray])}}) →json.dumps(.., XComEncoder)lèveTypeError: ndarray is not JSON serializable. Donc le backend ne transporte pas le payload s43 tel quel (ni sous, ni au-dessus du seuil — lejson.dumpsprécède le test). À élargir en test unitaire versionné (corpus réel + forme s43), mais le verdict est déjà tombé. - Conséquence sur la complétude : pour le parc existant (JSON-only, vérifié), B2 est complet → A. Pour numpy/s43, B2 a trouvé que le backend ne suffit pas → ce n'est pas une régression du flip (s43 n'envoie rien en XCom aujourd'hui), c'est un finding pour S02 : route X exige une stratégie de sérialisation numpy (
.tolist()+ dtype-tag, extensionXComEncoder/serde, ou route Y). Aucune contingence Tier-1 nécessaire : l'échec numpy est déterministe et déjà caractérisé, pas un type-surprise à chasser dans une instance parallèle.
C. Calibrage du seuil¶
Échantillonner la distribution de tailles XCom prod (#1102). Seuil > p99.9 du contrôle-flow (reco comité ; percentile + rationale dans l'ADR), < plancher payloads-data. Départ 1 MB. Aujourd'hui : contrôle-flow tout sous seuil (objectif zéro offload de l'existant).
D. TTL / lifecycle bucket¶
Lifecycle-rule S3 sur s3://<bucket>/xcom/ (expiration N jours) + sémantique clear/purge du backend documentée. Sweeper orphelins = Q6 (vs S03).
E. Rollout = A (recon-fondé) ; Tier-1 = contingence¶
Pas de subset-prod par-DAG (impossible). Pas de Tier-1 par défaut (la serde d'offload n'a aucun trafic réel à exercer — §0bis).
1. Tier-0 local — docker-compose : flip + round-trips ciblés (§B.2) + run d'un DAG mappé + launcher → near-inert confirmé.
2. Pre-flight prod (§G) — prouve provider + connexion S3 + config réellement prise in-pod.
3. Cut-over prod — values-prod.yaml → CI deploy Helm, kill-switch armé (§H). Flip near-inert (tout sous seuil). Rampe de seuil = contrôle PROSPECTIF, pas du jour-J : aujourd'hui rien n'offloade, donc elle ne gradualise rien au flip — elle sert quand un gros payload arrivera (post-S02). Ne pas la sur-vendre comme une mitigation du cut-over.
4. Vérif post-flip — échantillon élargi (DAGs à XCom divers, fréquence/volume, tâches longues, critiques trading), intégrité via #1102.
5. Contingence Tier-1 — uniquement si §B surprend (type non énuméré / round-trip rouge) : alors release Helm parallèle éphémère qui injecte les payloads réels + la forme offload (pas juste « rejouer », cf. §0bis). Non construit sinon.
F. ADR (frontière S04)¶
S01 = ADR de fondation (standard technique/invariant + percentile + pas de bespoke ; FS-RWX rejeté). S04 = guideline + review gate. (Q5.)
G. Pre-flight DURCI (footgun common.io)¶
scripts/xcom_flip_preflight.py (read-only, fail-fast). Le point dans common.io est un piège connu : selon shell/K8s l'env var AIRFLOW__COMMON_IO__... peut être silencieusement droppée (seuil drop → défaut -1 → flip inerte ; path drop → offload échoue). « Env var exportée » ≠ « Airflow l'a lue ». Donc le pre-flight prouve trois choses in-pod, pas deux :
1. apache-airflow-providers-common-io présent (airflow providers list).
2. Connexion S3 <conn_id> existe et fonctionne (write/read/delete sentinelle sous xcom/_preflight/).
3. La config a réellement pris : airflow config get-value common.io xcom_objectstorage_path et ... xcom_objectstorage_threshold retournent les valeurs attendues (≠ -1/vide). Bloque sinon (ADR-25). À exécuter dans le contexte worker/scheduler réel (pas un pod quelconque — l'injection env peut différer entre pods). (Hedge : que le point morde ou non sur notre 2.10.4+Helm, cette vérif est bonne — c'est la même méthode config get-value qui a déjà servi à prouver enable_xcom_pickling=False in-pod en v4.)
H. Runbook rollback / kill-switch (revert + re-trigger)¶
documentation/runbooks/runbook_xcom_backend_flip.md. kill-switch = revert AIRFLOW__CORE__XCOM_BACKEND → BaseXCom via Helm. Précision v4 (source lue) : le revert seul ne suffit pas si des XCom offloadés (> seuil) ont été écrits pendant la fenêtre de flip — la DB n'en garde que la string de chemin, que BaseXCom rendra telle quelle à l'aval (qui attend la donnée) → casse. Les XCom étant per-run, le fix est revert + re-trigger des DAG-runs in-flight (self-heal au re-run, pas de continuité mid-run). Aujourd'hui moot (rien n'offloade), mais load-bearing dès qu'un payload franchit le seuil (s43 sous une route numpy). À répéter au rehearsal avant cut-over. Downtime attendu + vérif post-rollback documentés.
§S02. Connexion à s43 — route X n'est PAS drop-in (finding v4)¶
Avant v4, l'hypothèse était : s43 passe return/pull direct sur le backend (route X), son smoke = test d'acceptation de l'offload. Le round-trip §B.2 l'invalide : XComObjectStorageBackend (1.4.2) ne sérialise pas numpy → un s43 qui return-erait son dict-d'arrays via XCom crasherait au json.dumps(XComEncoder), à toute taille. Donc route X exige une décision de sérialisation en S02, pas un simple return/pull :
- X′ : s43 convertit arrays→listes + dtype/shape-tag avant return (le backend offloade alors du JSON volumineux) — coûte la fidélité dtype + le volume JSON ;
- X″ : étendre XComEncoder/enregistrer un serializer numpy (serde airflow.serialization) — fait du backend un vrai transport numpy, bénéficie à tout le parc futur ;
- Y (préférence opérateur, 2026-06-05) : pass-by-référence — XCom = pointeur (s3://…/file.npz), données en store (cvntrade_s3_manager/npz/parquet). Robuste, classique, ≈ l'archi s43 actuelle (qui sépare déjà clés et arrays). Le plus sain pour numpy/gros objets ML ; XCom transporte la référence, pas la donnée. À confirmer en S02.
Impact sur S01 : aucun (S01 = flip near-inert du parc JSON ; il ne dépend d'aucun trafic numpy). Mais S01 livre ce finding à S02 et corrige l'Epic §2. Le « S01 valide l'offload, s43 le valide in-vivo » de v3 tombe : l'offload numpy n'est pas fourni par le backend nu — c'est précisément ce que S02 doit trancher.
Files¶
infra/helm/airflow/values-prod.yaml— 4 env vars (cut-over ; staged).scripts/xcom_serialization_audit.py+scripts/test_xcom_roundtrip.py(round-trips ciblés inline+offload) +tests/unit/....scripts/xcom_flip_preflight.py(+ test) — §G durci.documentation/runbooks/runbook_xcom_backend_flip.md— §H.documentation/adr/00NN-xcom-objectstorage-transport-standard.md+ entréedocumentation/ADR.md.documentation/stories/CVN-N014-ED-S01/index.md(hub) +mlops_readiness.md.- (infra) lifecycle-rule S3
xcom/. - (contingence, non construit par défaut)
infra/helm/airflow/values-staging.yaml— Tier-1 blue/green si §B surprend.
Risks & mitigations¶
- Faux sentiment de sécurité du « near-inert » → c'est parce que tout est sous seuil ; la rampe de seuil + le round-trip offload ciblé (§B.2) couvrent le moment où un payload franchira (s43). Pas de complaisance : le pre-flight prouve la config, le kill-switch est armé.
- Footgun
common.io(env var droppée silencieusement) → pre-flight §G.3 (config get-value). - Audit dément la recon (type non énuméré) → contingence Tier-1 (§E.5), pas de downgrade.
- Provider/connexion absents → pre-flight §G.1/2.
- S3 indisponible runtime → écriture > seuil échoue, tâche échoue (fail-loud, ADR-25) → runbook.
- Seuil mal réglé → p99.9 sur mesure + rampe.
- Accumulation S3 → TTL/lifecycle ; sweeper = Q6.
Success criteria¶
- Pre-flight vert (provider + S3 + config prise in-pod) avant flip.
- Audit publié + round-trips ciblés verts (JSON inline + numpy s43 offload) : complétude prouvée ou contingence déclenchée explicitement.
- Seuil (p99.9 documenté) + TTL + path configurés.
- Cut-over prod sans régression (échantillon élargi, #1102) ; kill-switch répété avant cut-over.
- ADR mergé (+ live docs.cvntrade.eu).
Questions for the committee (re-plan_review)¶
- Q1 — Backend class + config corrects pour 2.10.4 ? Connexion S3 à réutiliser ? (pre-flight §G la prouve.)
- Q2 — Seuil p99.9/p99.99 + rampe : bonne combinaison ?
- Q3 (recadré) — Vu
enable_xcom_pickling=False+ zéro trafic offload aujourd'hui, un audit statique + round-trips ciblés (JSON inline + numpy s43) suffit-il à prouver la complétude et prendre A, rendant le Tier-1 blue/green inutile ? (Et accepte-t-on que C, tel que spécifié en v2, validait le chemin bénin — qu'même C devrait injecter le numpy s43 pour valoir ?) - Q4 (recadré) — Si le comité maintient une instance hors-prod par défense en profondeur : exige-t-il l'injection du payload offload (sinon elle est aveugle), et reconnaît-il que c'est alors un round-trip ciblé, pas un « replay des vrais DAGs » ?
- Q5 — Frontière ADR S01 (standard) vs S04 (guideline + review gate) ?
- Q6 —
clear/purge+ lifecycle-rule suffisent, ou sweeper d'orphelins dès S01 ? - Q7 (recadré v4) — Finding vérifié :
XComObjectStorageBackend1.4.2 ne sérialise pas numpy (l'Epic §2 « native numpy » est faux). Quelle stratégie pour S02 — X′ (arrays→listes+dtype-tag), X″ (extensionXComEncoder/serde numpy, bénéficie au parc futur), ou Y (store bespoke, XCom=clés) ? X″ est-il dans le scope du standard (S01/S04) plutôt que de s43 seul ? - Q8 (correction Epic) — Faut-il corriger l'Epic CVN-N014-ED §2 (retirer « sérialise nativement numpy » → « JSON natif ; numpy/pandas exigent un serializer enregistré ou un pass-by-référence explicite ») ? Ça reclasse le standard : excellent pour le contrôle-flow + pass-by-référence de blobs déjà sérialisés, pas un transport numpy transparent.
Plan_review — outcome¶
PASSED (session 8d7efca0, OP Meeting #249, code OK, consensus strong, 0 blocker) — gate In specification → Specified satisfait. Recommandations (non-bloquantes, portées en implémentation) :
1. S3 IAM least-privilege sur le préfixe XCom (read/write composants Airflow + lifecycle) — à documenter dans l'ADR/runbook.
2. Round-trips d'offload automatisés en CI (gros payloads JSON) pour valider le chemin d'offload en continu, même sans trafic prod.
3. Audit récurrent (scripts/xcom_serialization_audit.py, ex. trimestriel) pour détecter de nouveaux types XCom cassant l'hypothèse near-inert.
4. Décision numpy S02 (X′/X″/Y, Y préféré) + MAJ Epic §2 — fait (commit 00e3d415).
5. Rehearsal obligatoire du kill-switch revert+re-trigger avant cut-over (gate documenté).
6. Monitoring/SLO XCom post-deploy : taux d'offload, latence, distribution de tailles, erreurs inline/offload.
7. Audit statique sur le jeu de DAGs le plus large (deux dépôts) pour minimiser le risque de type non-énuméré.
Committee launch (ready)¶
python scripts/expert_committee.py \
--artifact documentation/reviews/2026-06-05-cvn-n014-ed-s01-fondation-flip-global-du-backend-object-storage-xcom-load-bearing-plan.md \
--question "<English question covering Q1–Q7, leading with the recon-founded A>" \
--session-type plan_review --issue "#1105"
Consolidation¶
S01 est la fondation load-bearing de l'Epic CVN-N014-ED : elle bascule globalement le backend XCom vers l'object-storage (Route A, recon-fondée — l'audit statique a prouvé un parc near-inert, rendant le blue/green Tier-1 inutile), de sorte que return/xcom_pull offloade automatiquement les gros payloads sans qu'aucun DAG ne bricole un stockage intermédiaire. C'est la pré-condition des trois Stories suivantes : S02 (s43 en pass-by-référence S3, le premier bénéficiaire/validateur), S03 (inventaire des workarounds — vide grâce au flip), S04 (standard écrit + gate de review qui défend l'acquis).
Ce que S01 a livré : le flip global du backend, une décision numpy assumée (Route Y — XCom porte des références, pas des arrays bruts — après le finding v4 que XComObjectStorageBackend ne sérialise pas numpy nativement, corrigé dans l'Epic §2), un lifecycle S3 (xcom/ 7 j), et un kill-switch revert+re-trigger répété avant cut-over.
L'enseignement durable : le cut-over a d'abord échoué sur un encodage de section dottée (common.io → variable d'env AIRFLOW__COMMON_IO__, underscore, pas COMMON.IO) — un footgun invisible au env | grep. RCA + fix-forward ont donné la règle « vérifier la résolution de la config (conf.get/valeur effective), jamais sa simple présence » (PRs #1118/#1119). S01 a fermé Closed, le flip live et validé (working-set XCom offloadé observé), transférant à S04 la prévention des régressions. Cette section consolide l'apport au sens d'ADR-0101 (chapitre de clôture) — gate epic-close Gate 2 satisfait rétroactivement lors de la clôture de l'Epic.