ADR-038 — Modèle d’exécution : runtime async natif io_uring (sans tokio), couches basses synchrones, io_uring seul moteur de readiness
Statut : Accepté (2026-06-24, validé par le BDFL). AMENDÉ par ADR-092
(2026-07-11) : le runtime async, placé ici « couche 2 » sous le nom air-event, se scinde
en un réacteur couche 1 air-uring (rempart safe sur io_uring, contrainte check-layers :
couche 2 ↛ couche 0) + un exécuteur couche 2 renommé air-async (le nom air-async remplace
air-event). Le modèle (natif io_uring, sync-first / async opt-in, io_uring seul moteur de readiness)
est inchangé — seuls le découpage en couches et le nom changent.
Amende et précise ADR-023 (runtime asynchrone Air sur
io_uring) ; s’appuie sur ADR-022 (architecture
io_uring). Met à jour la macro-architecture : les mentions « air-event
au-dessus de tokio » deviennent caduques.
Catégorie : Architecture (couches 1/2) / Méthode.
Contexte
tokio avait été envisagé au tout début, avant que le support intégral
d’io_uring ne soit acquis. Ce n’est plus le cas : le module io_uring est complet
(12 Temps, couche 0 scellée couche-0-v1) et fournit nativement readiness (POLL_ADD,
multishot), complétion, timeouts, et un eventfd de complétion enregistrable
(IORING_REGISTER_EVENTFD).
Deux questions en découlent, posées par le BDFL :
- Le système étant parti sur une exécution asynchrone, quels composants/services ont réellement besoin d’un traitement synchrone ?
- Faut-il redescendre en couche 0 implémenter le syscall
epoll?
Décision
1. Pas de tokio — air-event est un runtime async natif sur io_uring
Air n’a plus aucune dépendance à tokio. Le runloop / runtime asynchrone
(air-event, couche 2) est notre cœur maison bâti sur io_uring (cohérent
ADR-023). Cohérent avec la stratégie « maison, sans dépendance lourde non maîtrisée »
et avec le déterminisme de build (ADR-025).
2. Posture synchrone-first (couches 0/1) + asynchrone opt-in (couche 2)
Le système n’est pas « tout-asynchrone ». Les couches basses sont synchrones :
les wrappers couche 0 bloquent, et les API couche 1 (air-socket,
air-filesystem, air-process…) sont synchrones, exposant as_fd() comme
couture (seam) d’intégration. air-event (couche 2) est le moteur de
concurrence async, opt-in — jamais imposé.
Composants/services qui doivent rester synchrones (le synchrone est de première classe) :
- Initialisation avant runtime + sécurité : parse config/arguments,
drop_privileges, seccomp, Landlock — exécutés avant que l’event loop existe ; ordonnés, sécurité-critique. - Outils one-shot / CLI (
air-call,air-trust-tool, petites.airappconsole) : monter un runtime async pour « faire une chose et sortir » n’a pas de sens. - Travail compute-bound (
air-crypto,air-memory, parsing) : aucun I/O → l’async n’apporte rien. - Embarqueurs avec leur propre boucle (GLib, boucle de jeu, runtime d’un langage
hôte) : ils intègrent nos fd (seam
as_fd()) dans leur boucle ; Air ne doit pas imposer son runtime. - RAII / teardown /
Drop; poignées de main systemd (sd-notify).
3. io_uring est le seul moteur de readiness + complétion — pas d’epoll
io_uring remplace epoll : readiness via POLL_ADD/IORING_POLL_ADD_MULTI,
timeouts, complétions, et register_eventfd pour l’interopérabilité. Ajouter epoll
serait un second mécanisme d’événements redondant à maintenir, à rebours de
l’ethos « un mécanisme propre ». On n’implémente donc pas epoll.
Porte laissée ouverte. Si un besoin réel et mesuré d’epoll émerge un jour, on réexaminera par RFC. Mais tant qu’io_uring fait le travail, on s’en contente — io_uring seul.
4. Multiplexage synchrone rare de quelques fd
Pour le cas (rare) d’un code synchrone devant attendre plusieurs fd : soit
io_uring en mode submit-and-wait (il fonctionne très bien en synchrone), soit —
si l’on veut un multiplex sync sans tirer io_uring — un futur petit wrapper
ppoll(2) (bien plus simple qu’epoll). La couche 0 étant scellée, un tel ajout
passerait par RFC et ne se ferait que sur besoin concret. Pas d’epoll dans ce
cas non plus. Constat : le client DNS d’air-socket (synchrone, livré) gère déjà ses
timeouts en non-bloquant + clock_nanosleep, sans epoll ni poll.
5. Embarquement dans une boucle hôte (y compris epoll-based)
io_uring expose un eventfd de complétion (IORING_REGISTER_EVENTFD) : une boucle
hôte (sélecteur, epoll, GLib…) attend ce seul fd pour savoir qu’Air a du travail
prêt. Air interopère ainsi avec des boucles epoll externes sans implémenter
epoll. (io_uring sait même piloter un epoll existant via IORING_OP_EPOLL_CTL si
le besoin se présentait.)
Conséquences
- Un seul mécanisme d’événements (io_uring) : pas de seconde pile de readiness à écrire, tester, maintenir.
- Chemin synchrone de première classe : pas de « taxe async » sur le code d’init, de sécurité, CLI, compute-bound, ou pour les embarqueurs.
- Zéro dépendance tokio : contrôle total, déterminisme (ADR-025), cohérence avec la stratégie maison.
- Mises à jour documentaires : la macro-architecture (mentions « au-dessus de
tokio ») et le framing d’ADR-023 sont corrigés ; la spec
air-events’écrira sur cette base (runtime io_uring natif). - Les primitives de synchronisation bloquantes de couche 1 (
air-thread) restent adossées àstd::sync(cf. specair-thread), pas àtokio::sync.
Alternatives écartées
- Garder tokio : dépendance lourde et runtime non maîtrisé, redondant avec le runtime io_uring natif désormais disponible ; contraire au contrôle/déterminisme.
- Implémenter
epollen couche 0 : redondant avec io_uring (poll/multishot/ timeouts) ; seconde pile d’événements sans gain tant qu’io_uring suffit. (Rejeté maintenant, ré-examinable par RFC si besoin réel.) - Tout-asynchrone obligatoire : pénalise l’init, les outils CLI, le compute et les embarqueurs ; complexité imposée sans contrepartie.
Adoption
Édition documentaire immédiate : ADR-038 + correction des mentions tokio de la
macro-architecture. La spec de composant air-event (couche 2) sera rédigée sur la
base de cet ADR (runtime async natif io_uring, API C-ABI, intégration as_fd() /
eventfd). Un éventuel ppoll ou epoll reste hors périmètre sauf RFC ultérieur
motivé par un besoin mesuré.