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.