ADR-058 — loom : activation du modèle de concurrence (outil de vérification test-only)
Statut : Accepté (2026-07-01, par délégation d’autonomie BDFL pour ce lot ; gravé pour revue
a posteriori). RFC de structure (ADR-015). Companion de
ADR-030 (dépendances test-only, jamais livrées), ADR-021
(EINTR remonté, la boucle EST la reprise), ADR-031 (la vraie garantie
= couverture per-crate, pas un plancher global), et de la roadmap
docs/roadmap-couche1-libc-pal-fr.md (§3.1).
Catégorie : Outillage de vérification (test-only). Pas de surface livrée, pas d’impact ABI.
Contexte
La variance run-to-run de la couverture lignes (95,89 ↔ 96,12 %, qui a forcé le plancher
96→94, #189) provient de branches de contention concurrentes (chemins LOCKED/
LOCKED_CONTENDED des primitives futex) dont l’exécution dépend de l’ordonnancement : une
ligne est couverte certains runs seulement. Un test classique ne peut pas forcer
déterministement un entrelacement précis.
loom (projet tokio-rs) est l’outil Rust de référence pour
cela : sous --cfg loom, il remplace le modèle mémoire (atomiques, UnsafeCell, threads) par
un ordonnanceur qui explore exhaustivement tous les entrelacements d’un modèle borné, et
signale toute violation (course de données, non-exclusion). Il prouve la sûreté d’un protocole
concurrent, là où un test ordinaire ne fait qu’échantillonner un ordonnancement.
Le pré-câblage existait déjà (workspace : cfg(loom) déclaré + alias d’atomique commuté ;
air-sys-syscall : un modèle loom_refcount dormant), mais la dépendance n’était pas
activée — le commentaire du Cargo.toml de tête notait explicitement « décision de gouvernance
Principe 6 / ADR-024 en attente ». Cette ADR tranche cette attente.
Décision
-
Activer
loomcomme outil de modèle de concurrence, dépendance test-only tirée uniquement sous--cfg loom([target.'cfg(loom)'.dependencies]), hors du graphe de build normal — donc jamais livrée (ADR-030). Ce n’est pas une exception à la règle des 80 % (ADR-024, qui vise les dépendances de production) :loomest un outil de vérification, au même titre queproptest(déjà dev-dep du workspace, sans entréeEXCEPTIONS.md). Il n’entre donc pas dansdocs/EXCEPTIONS.md. LicenceMIT(liste blanchedeny.toml). -
Idiome d’aliasing (recommandation loom, déjà l’intention du commentaire workspace) : le même code de production est exercé sous loom et en production, via un alias d’atomique commuté
core/loom. Seul l’AirMutexd’air-threadest modélisé dans ce premier lot ;AirRwLock/AirSemaphore(et les autres primitives) suivront — d’où un alias local à l’AirMutex, pas global. -
Shim de blocage sous loom. loom ne modélise pas
futex(2). Le chemin contendu (futex_wait) est remplacé, sous--cfg loom, par un réveil coopératif (loom::thread::yield_now) et le réveil (futex_wake) par un no-op : la boucle du chemin lent relit l’état à sa prochaine planification (unfutex_waita toujours le droit de revenir « spurious » — la boucle EST la reprise, ADR-021 §2). Ce modèle est sound pour la SÛRETÉ (l’exclusion mutuelle ne dépend que de la sémantique atomiqueCAS/swap, fidèlement modélisée par loom). -
Garde-fou de fidélité (non négociable). loom prouve la sûreté (exclusion mutuelle, absence de course sur la donnée protégée) ; il ne prouve pas la vivacité (le réveil
futexréel), qui reste validée on-target (selftest 2 arches + stress). Un modèle loom ne remplace jamais la preuve on-target — il ajoute une garantie déterministe sur le protocole atomique. (Piège du mock : une couverture verte sur un modèle simplifié ne vaut pas exécution réelle.) -
Tests réguliers hors du run loom. Les modules
#[cfg(test)]d’air-threadqui instancient des primitives sont gardés#[cfg(all(test, not(loom)))](ils créeraient des atomiques loom horsloom::model→ panique). Sous--cfg loom, seulmod loom_airmutexs’exécute. -
Invocation (documentée, pas dans la barrière bloquante pour l’instant) :
RUSTFLAGS="--cfg loom" cargo test -p air-thread --lib --release loom_airmutexUn job CI loom optionnel (hors barrière pré-merge) pourra suivre quand plusieurs primitives seront modélisées.
Conséquences
- Positif : les branches de contention de l’
AirMutexsont désormais couvertes par construction (tous les entrelacements), pas au hasard de l’ordonnancement → attaque directe de la variance de couverture. Base réutilisable (le shim futex-loom sert aussiair-alloc,AirRwLock,AirSemaphore). - Coût : sous
--cfg loom(jamais un build normal), les tests réguliers sont désactivés ; un recompile complet sous le cfg est nécessaire. Le pré-câblageloom_refcountdormant d’air-sys-syscallémet quelques warnings sous loom (pré-existant, hors périmètre de ce lot). - Neutre : zéro impact sur les builds de production, la couverture mesurée (le code
cfg(loom)n’est pas compilé hors loom), l’ABI, la cible*-linux-air.
Alternatives rejetées
- Rendez-vous déterministe par test (barrières/atomiques dans le test) : marche au cas par cas (déjà employé, #187) mais ne systématise pas et n’explore pas l’espace des entrelacements. Complémentaire, pas substitut.
- Réécrire un ordonnanceur maison : refaire loom, coûteux et moins éprouvé.
shuttle(AWS) : exploration randomisée-rejouable plutôt qu’exhaustive ; loom (exhaustif, borné) est préférable pour prouver la sûreté d’un petit protocole.
Suite
- Étendre les modèles loom à
AirRwLock,AirSemaphore, puis au verrou futex d’air-alloc(même shim). Puis, quand plusieurs modèles existent, envisager un job CI loom optionnel. - Nettoyer les warnings loom dormants d’
air-sys-syscall(loom_refcount) — hors périmètre ici.