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.