Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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). Or check-layers interdit à 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 :

  1. air-uring — le réacteur, COUCHE 1. Rempart safe au-dessus de l’unsafe io_uring de la couche 0 (exactement comme air-poll expose ppoll, ADR-044). Il possède tous les types io_uring (SQE/CQE, SubmissionToken, buffers possédés) et mappe token → Waker. layer = 1.

  2. 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.

  3. Nommage : air-async remplace air-event (amende ADR-038). L’exécuteur couche 2 s’appelle air-async ; le réacteur couche 1 s’appelle air-uring. La spec air-event (couche 2) d’ADR-038 devient la spec air-async ; les références air-event de la macro-architecture, de l’INDEX et des specs sont propagées en air-async (suivi de cohérence documentaire, ci-dessous).

  4. 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é dans air-sys-syscall::io_uring (submit_read/write(Vec<u8>)into_buffer_result, SubmissionToken opaque anti-ABA, submit_timeout/IORING_OP_TIMEOUT, register_eventfd). air-uring/air-async ne réinventent rien. v1 : mono-thread (IoUring est Send + !Sync), un CQE → un réveil de Waker, timers = IORING_OP_TIMEOUT (io_uring seul, pas timerfd/epoll — ADR-038). Preuve incrément 0 : block_on(timeout) traverse exécuteur L2 → réacteur L1 → io_uring L0 → CQE → Waker → Ready, check-layers vert.

  5. 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-layers a 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 sceau couche-1-v2.0 ne 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) consomment air-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âche en O(1) (le squelette balaie un Vec ; 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-async dans specs/layer-2/air-event.md (→ air-async.md, contenu à réviser pour refléter le split), INDEX.md, macro-architecture-fr.md, le schéma air-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-async dit mieux ce qu’est l’étage haut (l’exécuteur de futures) ; air-uring nomme 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-uring l’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-uring déclare layer = 1, air-async déclare layer = 2 — vérifié dans leurs [package.metadata.air], tenu par le gate cargo xtask check-layers. Consommé en production par air-sshd, air-com et air-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-async ne nomme aucun type io_uring.
  • Étendu depuis par ADR-098air-async : combinateurs de concurrence structurée join/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 :

  1. La spec air-event dé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, zone air-stable) bâtie sur air-runtime (couche 1) — cf. ADR-039 (« Nommage : runtime, objet, ordonnancement »). Or air-async tel qu’implémenté est un exécuteur Rust bâti sur air-uring. Le réacteur a changé, et la surface aussi. Renommer le fichier produirait une spec qui contredit le code.
  2. 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.so figure pourtant dans le contrat ABI 10 ans annoncé par la macro-architecture.
  3. Ni air-uring ni air-async n’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.