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-023 — Runtime asynchrone Air construit sur io_uring

Statut : Accepté. Document fondateur de la phase 1.

Catégorie : Architecture (couche 1).

Contexte

Air a besoin d’un runtime asynchrone pour orchestrer les opérations io_uring, gérer les futures Rust, scheduler les tâches. La couche 0 fournit les primitives kernel (io_uring, signalfd, timerfd) ; la couche 1 doit fournir une abstraction async ergonomique au-dessus.

Plusieurs options existent dans l’écosystème Rust :

Tokio : runtime de référence, très mature, écosystème énorme. Conçu initialement pour epoll, support io_uring via tokio-uring (crate annexe encore expérimentale). Très volumineux.

glommio : runtime conçu spécifiquement pour io_uring, architecture thread-per-core. Performances excellentes mais audience plus restreinte. Maintenu par DataDog.

smol : runtime minimaliste, écosystème async-std-compatible. Pas de support io_uring natif.

Runtime maison : construire un runtime spécifique à Air, intégré nativement avec la couche 0.

Décision

Air construit son propre runtime asynchrone, air-runtime, plutôt que d’adopter un runtime existant. Le runtime est conçu nativement sur io_uring (couche 0) et exclusif à Linux. Il vit en couche 1 et consomme directement l’API de air-sys-syscall::io_uring.

Le runtime n’est pas construit en phase 0 (qui se concentre sur la couche 0 et les types fondamentaux). Il est construit en phase 1, en parallèle du modèle d’objet C-ABI.

Justifications

Cinq raisons principales motivent la construction d’un runtime maison plutôt que l’adoption de Tokio, glommio, ou smol.

Premièrement, l’alignement avec io_uring. La couche 0 d’Air est entièrement spécifiée autour d’io_uring : signal handling via signalfd, timers via timerfd, sockets et fichiers via io_uring natif. Les runtimes existants ont été conçus pour epoll et portent cette dette dans leur design. Tokio supporte io_uring via une crate annexe encore expérimentale qui ne couvre qu’une fraction des opérations ; glommio est io_uring-natif mais avec une architecture thread-per-core qui ne correspond pas à tous les patterns Air ; smol n’a pas de support io_uring. Construire le runtime maison permet une cohérence native impossible à obtenir autrement.

Deuxièmement, le Principe d’ingénierie 6 (règle des 80 %). Tokio est massif : Air n’utiliserait au mieux que 20 à 30 % de sa surface API. La règle des 80 % impose soit le vendoring partiel (très coûteux pour un runtime async, qui est interconnecté), soit une exception explicite (qui serait substantielle pour un composant aussi structurant). Glommio et smol sont mieux placés mais imposent quand même des conventions et des dépendances qui ne sont pas les nôtres.

Troisièmement, le Principe d’ingénierie 7 (verbosité au service de la clarté). Tokio cache beaucoup de complexité derrière des macros (#[tokio::main], select!) et des conventions implicites (auto-wake, work-stealing, etc.). Un développeur Air doit pouvoir lire et comprendre tout le code qui exécute son application. Un runtime maison construit explicitement satisfait cette exigence par construction.

Quatrièmement, la leçon de l’expérience Tokio. L’écosystème Rust async a maintenant plusieurs années de recul sur Tokio. Plusieurs choix initiaux se sont révélés problématiques avec le temps : pinned futures qui demandent des hacks ergonomiques, ABI instable entre versions majeures, intégration io_uring difficile à rétrofitter. Construire un runtime à la lumière de cette expérience permet d’éviter ces écueils dès le début.

Cinquièmement, l’alignement philosophique avec le projet. Air construit son stack du bas vers le haut, avec ses propres conventions et son propre vocabulaire. Intégrer Tokio (ou tout runtime tiers majeur) en couche 1 introduirait une dissonance : la couche 0 est pure Air, la couche 1 emprunterait massivement, les couches 2+ reviendraient sur les conventions Air. La cohérence verticale du stack est un atout important pour la durée de vie du projet.

Périmètre du runtime

Le runtime maison se concentre sur ce dont Air a besoin, ni plus ni moins. Périmètre cible pour la phase 1 :

  • Un scheduler de tâches asynchrones (Task Air).
  • Un mécanisme d’attente de complétions io_uring intégré au scheduler.
  • Des primitives async pour les opérations courantes (sleep, timeout, select).
  • Des primitives de synchronisation async (mutex, channel, notify).
  • Une intégration avec signalfd pour la gestion des signaux async.
  • Une intégration avec timerfd pour les timers async.

Périmètre explicitement exclu (au moins pour la phase 1) :

  • Work-stealing scheduler complexe. Le modèle initial est thread-per-core ou simple round-robin selon les besoins.
  • Support multi-runtime (plusieurs instances coexistantes).
  • Compatibilité avec l’écosystème Tokio. Les crates tierces qui dépendent de Tokio ne fonctionneront pas avec air-runtime ; c’est assumé.

Ces exclusions peuvent être levées en phase 2 ou 3 si l’expérience montre un besoin réel.

Architecture proposée

Le runtime est organisé en plusieurs crates au sein du workspace Air :

air-runtime/
├── air-runtime-core/    -- Scheduler, Task, primitives de base
├── air-runtime-io/      -- Intégration io_uring
├── air-runtime-time/    -- Timers basés sur timerfd
├── air-runtime-signal/  -- Gestion des signaux async via signalfd
├── air-runtime-sync/    -- Mutex async, channels, notify
└── air-runtime/         -- Façade qui réexporte les modules nécessaires

Chaque sous-crate suit les Principes d’ingénierie Air (test exhaustif, validation amont, etc.). La crate façade air-runtime est ce que les couches supérieures consomment.

Coût estimé

Construire un runtime async Rust de qualité production sur io_uring représente plusieurs mois-personne. C’est un investissement non négligeable mais qui s’amortit sur la durée de vie du projet (10+ ans selon la promesse de stabilité ABI). Comparé au coût d’une dépendance externe massive comme Tokio (audit continu, migrations forcées, friction d’intégration), l’investissement est défendable.

Statut futur

ADR immuable dans son intention. L’ajout de fonctionnalités au runtime au fil des phases (work-stealing scheduler en phase 3 si nécessaire, par exemple) est de la responsabilité du développement normal et ne requiert pas d’amendement.

Si l’expérience montre que le runtime maison est intenable (ce qui serait surprenant mais possible), un RFC explicite peut le remplacer par un runtime externe. Cette éventualité est mentionnée pour transparence ; elle n’est pas anticipée.


Licence du document : MPL 2.0 Statut : Document fondateur immuable.