ADR-092 — air-async : découpage réacteur (couche 1) / exécuteur (couche 2) de la runloop io_uring
Statut : Accepté (2026-07-11, décision BDFL). Raffine et renomme ADR-038
(runtime async natif io_uring) : le runtime, placé « couche 2 » par ADR-038, se scinde en un
réacteur couche 1 (air-uring) et un exécuteur couche 2 (air-async) ; le nom air-async
remplace air-event. Met en œuvre ADR-091 (air-async = le
pilote I/O async du motif sans-IO). Instruit par le rapport de design
design-air-async-fr.md (incrément 0, squelette prouvé).
Catégorie : Architecture (couche 2 / couche 1, fondateur — la runloop). Toute évolution
structurante passe par un RFC (ADR-015). S’appuie sur
ADR-022/ADR-028 (io_uring
couche 0), ADR-044 (précédent air-poll), et le gate
cargo xtask check-layers.
Contexte
Le portage std sur Air safe est bouclé (ADR-088). La couche 2 commence, et sa pièce maîtresse
est la runloop asynchrone (ADR-038 : runtime async natif io_uring, sans tokio, io_uring seul moteur
de readiness — nommée air-event, « couche 2 »). L’incrément 0 de design (squelette prouvé
on-host) a fait émerger une contrainte de layering que le gate check-layers (métadonnée
[package.metadata.air] layer) rend désormais dure :
Le réacteur touche io_uring, qui est couche 0 (
air-sys-syscall::io_uring, non transverse). Orcheck-layersinterdit à une couche 2 de dépendre de la couche 0 (M ∈ {N-1, N}). Un runtime async monolithique en couche 2 ne compile donc pas dans notre discipline.
Décision
La runloop se scinde en deux crates, seul découpage cohérent avec le layering et ADR-038 :
-
air-uring— le réacteur, COUCHE 1. Rempart safe au-dessus de l’unsafeio_uring de la couche 0 (exactement commeair-pollexposeppoll, ADR-044). Il possède tous les types io_uring (SQE/CQE,SubmissionToken, buffers possédés) et mappetoken → Waker.layer = 1. -
air-async— l’exécuteur, COUCHE 2. Futures /block_on/ spawn / timers — le pilote I/O async du motif sans-IO (ADR-091), consommé par le futur framework réseau. Il ne nomme aucun type io_uring (jetons opaques,Errno).layer = 2. ADR-038 est respecté : l’exécuteur reste couche 2 ; seul le réacteur descend en couche 1. -
Nommage :
air-asyncremplaceair-event(amende ADR-038). L’exécuteur couche 2 s’appelleair-async; le réacteur couche 1 s’appelleair-uring. La specair-event(couche 2) d’ADR-038 devient la specair-async; les référencesair-eventde la macro-architecture, de l’INDEX et des specs sont propagées enair-async(suivi de cohérence documentaire, ci-dessous). -
Modèle d’exécution (aligné ADR-038) — le socle est déjà en couche 0. Le modèle owned-buffer (« le buffer survit à la complétion », façon
tokio-uring) est déjà implémenté dansair-sys-syscall::io_uring(submit_read/write(Vec<u8>)→into_buffer_result,SubmissionTokenopaque anti-ABA,submit_timeout/IORING_OP_TIMEOUT,register_eventfd).air-uring/air-asyncne réinventent rien. v1 : mono-thread (IoUringestSend + !Sync), un CQE → un réveil deWaker, timers =IORING_OP_TIMEOUT(io_uring seul, pastimerfd/epoll — ADR-038). Preuve incrément 0 :block_on(timeout)traverse exécuteur L2 → réacteur L1 → io_uring L0 → CQE → Waker → Ready,check-layersvert. -
Frontière nette (invariant de revue).
air-uring= tout l’io_uring ;air-async= zéro type io_uring. Un consommateur (framework réseau, cœur de protocole sans-IO) ne dépend que d’air-async.
Conséquences
- Le gate
check-layersa produit l’architecture : la contrainte de layering (métadonnée + gate, mergés #328) a forcé un découpage propre plutôt que de le laisser à la vigilance humaine — validation concrète de cette discipline. air-uring(couche 1) est un nouvel additif couche 1 (la couche 1 reste évolutive — le sceaucouche-1-v2.0ne fige pas l’ajout de briques ; RFC/descellement additif au besoin).air-async(couche 2) inaugure la couche 2.- Prérequis de la suite : le framework réseau, l’IPC, et
air-sshd(démonstration couche 2) consommentair-async; les cœurs de protocole sans-IO (ADR-091) s’y branchent par le pilote I/O. - Questions ouvertes (incrément 1 et suivants) : sûreté d’annulation au drop (le buffer doit
vivre jusqu’au CQE d’annulation — le piège io_uring), corrélation
token → tâcheen O(1) (le squelette balaie unVec; prod = index par slot, éventuel RFC couche 0), spawn multi-tâches + roue de timers, éveil inter-thread (eventfd+msg_ring) et bascule thread-per-core, intégration boucle hôte (register_eventfd+as_fd()), back-pressure de soumission (EBUSY). - Suivi de cohérence documentaire : propagation
air-event → air-asyncdansspecs/layer-2/air-event.md(→air-async.md, contenu à réviser pour refléter le split),INDEX.md,macro-architecture-fr.md, le schémaair-couches-crates.svg. Traité en passe doc dédiée (le contenu de la spec doit refléter le découpage réacteur/exécuteur, pas un simple rename).
Alternatives rejetées
- Runtime async monolithique en couche 2 (ADR-038 littéral) : rejeté — ne compile pas sous
check-layers(couche 2 → io_uring couche 0 = saut interdit). Contourner en rendant io_uring « transverse » banaliserait la frontière kernel-unsafe la plus sensible. - Tout en couche 1 (réacteur + exécuteur) : rejeté — l’exécuteur/futures est de la surface applicative (couche 2 par ADR-038) ; le mettre en couche 1 brouille « couche 1 = rempart safe sur le kernel / couche 2 = surface haute ».
- Garder le nom
air-event: rejeté par le BDFL —air-asyncdit mieux ce qu’est l’étage haut (l’exécuteur de futures) ;air-uringnomme sans ambiguïté le réacteur. ADR-038 amendé en conséquence. - Réimplémenter le modèle owned-buffer / réinventer io_uring : rejeté — déjà fait et prouvé en
couche 0 ;
air-uringl’enveloppe, ne le duplique pas.
Mise en œuvre — état au 2026-08-02
Section factuelle, tenue à jour ; elle ne modifie aucune décision ci-dessus. L’ADR est
marqué fait au registre : sa substance — le découpage — est livrée. Cette section
existe pour que la dette documentaire qui subsiste ne disparaisse pas derrière ce fait.
Fait
- Le découpage lui-même, objet propre de cet ADR :
air-uringdéclarelayer = 1,air-asyncdéclarelayer = 2— vérifié dans leurs[package.metadata.air], tenu par le gatecargo xtask check-layers. Consommé en production parair-sshd,air-cometair-notifyd. - Quatre incréments mergés (#329 → #332) : squelette et preuve de traversée
L2 → L1 → L0 → CQE →
Waker(inc. 0) ; sûreté d’annulation io_uring, exécuteur, echo TCP async (inc. 1) ; éveil inter-thread, timers, back-pressure, I/O fichier (inc. 2) ; thread-per-core, multishot, provided buffers (inc. 3). Le point 4 annonçait un v1 mono-thread : l’inc. 3 est allé au-delà. - La frontière du point 5 tient :
air-asyncne nomme aucun type io_uring. - Étendu depuis par ADR-098
(«
air-async: combinateurs de concurrence structuréejoin/join_all/try_join,Unordered,Notify»), lui aussi fait.
Dette ouverte — le point 3 n’est pas soldé
Le point 3 de la Décision ne se limite pas à choisir un nom : il demande que « les références
air-event de la macro-architecture, de l’INDEX et des specs soient propagées en
air-async ». Ce n’est pas fait. Le nom air-event subsiste dans 26 occurrences hors
ADR : docs/SUMMARY.md (1), docs/architecture/macro-architecture-fr.md (12, dont la
liste des crates de la couche 2, la section « Event loop unifié » et libair-event.so),
docs/architecture/air-couches-crates.svg (1), docs/specs/layer-1/air-runtime.md (4),
docs/specs/layer-1/air-filesystem.md (1), docs/specs/layer-2/air-com.md (2),
docs/specs/layer-2/air-object.md (2), docs/notes/design-air-async-fr.md (3) — plus la
spec docs/specs/layer-2/air-event.md entière (23).
(Les occurrences dans ADR-038 et ADR-039 ne sont pas comptées et ne doivent pas être touchées : un ADR enregistre une décision datée et ne se réécrit pas — ADR-038 porte déjà en tête sa note de scission.)
Pourquoi ce n’est pas un renommage mécanique
La propagation bute sur une question de conception non tranchée :
- La spec
air-eventdécrit autre chose que ce qui a été implémenté. Elle spécifie une façade C-ABI (air_event_loop_run,libair-event.so, zoneair-stable) bâtie surair-runtime(couche 1) — cf. ADR-039 (« Nommage : runtime, objet, ordonnancement »). Orair-asynctel qu’implémenté est un exécuteur Rust bâti surair-uring. Le réacteur a changé, et la surface aussi. Renommer le fichier produirait une spec qui contredit le code. - La façade C-ABI reste-t-elle au programme, et sous quel nom ? Cet ADR ne le dit pas :
il nomme l’exécuteur, pas la façade.
libair-event.sofigure pourtant dans le contrat ABI 10 ans annoncé par la macro-architecture. - Ni
air-uringniair-asyncn’ont de spec de composant.docs/specs/layer-2/n’en contient aucune ; la seule spec du domaine est celle, périmée, d’air-event.
Tant que (1) et (2) ne sont pas tranchés — par RFC selon ADR-015 (« Licence MPL 2.0, contribution DCO, gouvernance évolutive ») si la surface ABI est touchée —, la propagation est bloquée par une décision, pas par du travail mécanique.