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-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 :

  1. Le système étant parti sur une exécution asynchrone, quels composants/services ont réellement besoin d’un traitement synchrone ?
  2. 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 .airapp console) : 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-event s’écrira sur cette base (runtime io_uring natif).
  • Les primitives de synchronisation bloquantes de couche 1 (air-thread) restent adossées à std::sync (cf. spec air-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 epoll en 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é.