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

Spec couche 1 — air-runtime (runtime asynchrone natif io_uring)

Spécification technique — Version 1.0 (décisions validées BDFL 2026-06-24). Couche 1 « Primitives système ».

Position et méthode

air-runtime est le runtime d’exécution asynchrone d’Air : ordonnanceur de tâches, réacteur io_uring, primitives async (sleep/timeout/select, mutex/channel/notify), signaux et timers async. Il vit en couche 1, consomme directement air-sys-syscall::io_uring (couche 0), et n’a aucune dépendance externe — en particulier pas de tokio (ADR-038). Il s’appuie sur air-base-lib (AirError/AirResult, AirInstant/AirDuration).

Décisions de socle déjà gravées (ne sont PAS rouvertes ici) :

  • Pas de tokio, runtime maison sur io_uring — ADR-023, ADR-038.
  • Nom air-runtime (couche 1) ; le modèle d’objet est air-object (couche 2) ; air-event (couche 2) est la façade C-ABI par-dessus ce runtime — ADR-039.
  • Synchrone-first / async opt-in : ce runtime n’est jamais imposé. Les API couche 1 (air-socket/air-filesystem/…) restent synchrones et exposent as_fd() ; air-runtime les pilote quand un consommateur choisit l’async.
  • Ordonnancement : exécuteur single-thread comme unité, scalable en thread-per-core shared-nothing (réutilise io_uring Temps 3e : RingPool/msg_ring/LockedIoUring/SqpollIoUring), sans work-stealing v1.
  • Soundness io_uring : tout repose sur les garanties de ADR-028 (S1/S2/S3, téardown).

Découpage multi-crate (ADR-039)

air-runtime-core/    — exécuteur single-thread, Task, spawn, block_on, réacteur
air-runtime-io/      — intégration io_uring : futures sur complétions, ownership buffers
air-runtime-time/    — sleep / timeout / intervalle (io_uring timeout ; timerfd si besoin)
air-runtime-signal/  — signaux async (signalfd lu via io_uring) — cf. ADR-020
air-runtime-sync/    — Mutex / channel MPSC / Notify **async** (≠ air-thread, bloquant)
air-runtime/         — façade qui réexporte la surface consommée par les couches hautes

air-runtime-sync est distinct d’air-thread (Principe : deux familles de synchro, ADR-038/039) : air-thread = bloquant (std::sync/parking_lot) ; air-runtime-sync = async (réveil par waker, pas de blocage de thread OS).


DÉCISION CENTRALE (validée BDFL 2026-06-24) — modèle de futures sur complétions io_uring

C’est le choix structurant : il détermine toute la surface I/O async.

Le problème. io_uring est à complétion (≠ epoll, à readiness) : on soumet un SQE référençant un buffer, le kernel écrit plus tard dans ce buffer, puis poste un CQE. Le buffer doit rester valide jusqu’au CQE. Or une Future Rust peut être abandonnée (drop) avant le CQE (annulation, select!, timeout) → si le buffer est libéré, le kernel écrit dans de la mémoire libérée (UB). C’est l’écueil de soundness connu des runtimes io_uring.

Décision (validée) — modèle « buffers possédés » + reclaim différé à l’annulation :

  1. Les opérations I/O prennent possession du buffer et le rendent à la complétion :
    #![allow(unused)]
    fn main() {
    // au lieu de read(&mut buf) (readiness, tokio-style) :
    async fn read(&self, buf: AirIoBuf) -> (AirResult<usize>, AirIoBuf);
    }
    Le runtime détient le buffer dans le slot d’opération (slab à user_data, même schéma anti-ABA que le slab io_uring couche 0) jusqu’au CQE.
  2. À l’annulation (drop de la future avant CQE) : le runtime soumet un ASYNC_CANCEL et conserve buffer + slot vivants jusqu’au CQE d’annulation (reclaim différé) — jamais de use-after-free. Adossé à ADR-028 (cycle de vie S1/S2/S3, téardown).
  3. Téardown du runtime : drainage des opérations en vol avant libération (ADR-028).

Conséquence ergonomique assumée : l’API passe les buffers par valeur (moins fluide que &mut buf), mais sound par construction — cohérent avec la rigueur des couches fondatrices (Principe 1) et le « sur-sécuriser puis dégraisser » (Principe 5). Des helpers (lecture vers Vec réutilisé via air-memory, BufRing io_uring 3b pour la réception) atténuent le coût.

Alternatives écartées : buffers empruntés (&mut) → exigeraient bloquer au drop ou fuiter l’op (non-sound / non-ergonomique) ; rejeté.

Les sections 2+ ci-dessous reposent sur ce modèle ratifié.


Section 1 — air-runtime-core : exécuteur, Task, réacteur

/// Exécuteur **single-thread** (unité d'ordonnancement, ADR-039). Possède son ring
/// io_uring et sa file de tâches prêtes. Non `Send` (affinité thread).
pub struct AirRuntime { /* ring + slab d'ops + file de tâches + réacteur */ }

impl AirRuntime {
    /// Crée un runtime single-thread (un ring io_uring dimensionné).
    /// # Errors `AirError` si la création du ring échoue.
    pub fn new() -> AirResult<Self>;
    pub fn with_config(config: AirRuntimeConfig) -> AirResult<Self>;

    /// Exécute `future` jusqu'à complétion en pilotant le réacteur (point d'entrée
    /// synchrone d'un programme : `fn main` appelle `block_on`).
    pub fn block_on<F: Future>(&self, future: F) -> F::Output;

    /// Planifie une tâche concurrente ; rend un handle attendable/annulable.
    pub fn spawn<F: Future + 'static>(&self, future: F) -> AirTask<F::Output>;
}

/// Handle d'une tâche : `await` son résultat, ou `cancel()` (annulation coopérative).
pub struct AirTask<T> { /* … */ }
impl<T> AirTask<T> { pub fn cancel(self); /* Future for AirTask<T> -> T */ }

/// Config : taille du ring, mode (single-thread / per-core), SQPOLL on/off…
pub struct AirRuntimeConfig { /* … */ }

Réacteur (boucle interne). Une seule boucle : (a) draine la file des tâches prêtes (poll), (b) soumet les SQE accumulés, (c) attend des CQE (io_uring_enter), (d) pour chaque CQE : retrouve le slot par user_data, dépose le résultat, réveille le waker. Pas de thread de réacteur séparé en mode single-thread (le thread courant est le réacteur entre deux poll). Verbosité assumée (Principe 7) : aucune magie, la boucle est lisible.

Scheduling. File FIFO simple en v1 (round-robin des tâches prêtes), pas de work-stealing (ADR-039). Équité suffisante pour des charges I/O-bound.


Section 2 — air-runtime-io : futures sur complétions

Cœur du modèle « buffers possédés » (décision centrale). Expose :

  • AirIoBuf : buffer possédé (longueur initialisée + capacité), convertible depuis/vers Vec<u8> et tranches air-memory ; passé par valeur aux ops.
  • Enregistrement de fd : AirAsyncFd::new(borrowed_fd) — adapte un as_fd() d’une API couche 1 synchrone (air-socket/air-filesystem) pour soumettre des ops io_uring dessus. C’est la couture sync→async (ADR-038).
  • Opérations (par valeur, rendent le buffer) : read, write, accept, recv, send, connect, poll_ready, etc. — adossées aux opcodes io_uring couche 0 (Temps 2a/2b). Multishot (accept/recv) exposé via un stream (réutilise Temps 3d).
  • submit/await_cqe internes : un seul slab d’ops à user_data (bit de tag cohérent avec le slab couche 0, Temps 4) ; annulation = ASYNC_CANCEL + reclaim différé.

Sûreté. Aucune fonction unsafe exposée. Les blocs unsafe internes (soumission de SQE pointant un buffer, lecture du CQE) portent // SAFETY: référant ADR-028 (le buffer vit jusqu’au CQE par construction du slot). Miri sur les transitions poll→submit→complete→wake et sur l’annulation (reclaim différé). loom sur le réveil de waker concurrent (mode per-core).


Section 3 — air-runtime-time : sleep / timeout / intervalle

#![allow(unused)]
fn main() {
pub async fn sleep(duration: AirDuration);
pub async fn sleep_until(deadline: AirInstant);
/// Course « future vs délai » ; `None` si le délai expire d'abord.
pub async fn timeout<F: Future>(duration: AirDuration, future: F) -> Option<F::Output>;
pub struct AirInterval { /* tick périodique */ }
}

Décision (validée). Les délais internes du runtime s’appuient sur IORING_OP_TIMEOUT (io_uring natif, déjà couche 0 Temps 2c/3d) plutôt que sur un timerfd par timer — zéro fd par timer, intégré au même ring. timerfd reste disponible (couche 0) si un AirTimer exposable (façade air-event) le justifie.


Section 4 — air-runtime-signal : signaux async

Conforme ADR-020 (signalfd par défaut). signalfd est lu via io_uring → un stream async de signaux. Pas de handler async-signal-unsafe ; le masquage et la sémantique restent ceux de la couche 0.

#![allow(unused)]
fn main() {
pub struct AirSignals { /* signalfd enregistré */ }
impl AirSignals {
    pub fn watch(set: &[AirSignal]) -> AirResult<Self>;
    pub async fn next(&self) -> AirResult<AirSignalInfo>;
}
}

Section 5 — air-runtime-sync : primitives async

Distinctes des primitives bloquantes d’air-thread (ADR-038/039). Réveil par waker (pas de blocage de thread OS) :

#![allow(unused)]
fn main() {
pub struct AirAsyncMutex<T> { /* … */ }   // lock().await
pub struct AirNotify { /* … */ }          // notified().await / notify_one()
pub fn channel<T>(capacity: usize) -> (AirAsyncSender<T>, AirAsyncReceiver<T>); // MPSC, back-pressure
}

Back-pressure explicite (cohérent AirCom, ADR-001) : le channel borné bloque async l’émetteur quand plein (pas de gonflement mémoire silencieux).


Section 6 — Mode multi-cœur : thread-per-core (ADR-039)

Le scaling n’introduit pas de work-stealing : on lance N exécuteurs single-thread indépendants (un par cœur), shared-nothing, chacun son ring. Coordination via les primitives couche 0 déjà livrées (io_uring Temps 3e) :

  • msg_ring : envoi d’un message/tâche d’un cœur à un autre (pas de file partagée verrouillée).
  • RingPool (ATTACH_WQ) : partage du pool de workers kernel entre rings (workers bornés).
  • SqpollIoUring : option SQPOLL (réveil NEED_WAKEUP) pour la latence.
#![allow(unused)]
fn main() {
pub struct AirRuntimePerCore { /* un AirRuntime par cœur + msg_ring */ }
impl AirRuntimePerCore {
    pub fn with_cores(n: usize) -> AirResult<Self>;
    pub fn spawn_on(&self, core: usize, future: impl Future + Send + 'static);
}
}

CPU-bound : déporté vers un pool de threads bloquants (air-thread) via spawn_blocking-like, pour ne pas bloquer un réacteur.


Périmètre v1 / différé

  • v1 : exécuteur single-thread, réacteur io_uring, ops I/O (buffers possédés), sleep/timeout, signaux, sync async, thread-per-core de base.
  • Différé (ADR-023) : work-stealing, multi-runtime coexistants complexes, optimisations mesurées (Principe 5). Façade C-ABI = air-event (couche 2, spec séparée).

Dépendances (règle des 80 %, ADR-024)

  • air-sys-syscall (io_uring, signalfd, timerfd) + air-base-lib (erreurs, temps). Aucune dépendance externe (pas de tokio/mio/futures-rs : on définit nos propres Future/Waker au-dessus de core::task). À tracer dans DEPENDENCIES.md.

Stratégie de tests

  • Couverture 100 % lignes + branches (Principe 1).
  • Miri : transitions de futures (poll/submit/complete/wake), annulation + reclaim différé (pas d’UB, pas de use-after-free du buffer), téardown (ADR-028).
  • loom : réveils de waker concurrents, msg_ring inter-cœurs, primitives sync async.
  • Property-based : séquences spawn/cancel/await ; invariants du slab d’ops.
  • Tests d’intégration réels : echo TCP/UDP via air-socket piloté par le runtime ; timeouts ; signaux (signalfd réel) ; charge thread-per-core.
  • (Pas de fuzzing dédié : aucun parsing de données externes — le runtime orchestre, il ne décode pas. Le décodage des CQE bruts est couvert/fuzzé en couche 0.)

Décisions de fond

  1. Buffers possédés + reclaim différé à l’annulation (décision centrale, à valider) — soundness io_uring par construction (ADR-028).
  2. Single-thread comme unité, thread-per-core sans work-stealing (ADR-039, Principe 9).
  3. io_uring seul : sleep/timeout/signaux via io_uring/signalfd, pas d’epoll (ADR-038).
  4. Deux familles de synchro : async (ici) vs bloquante (air-thread).
  5. Façade C-ABI séparée : air-event (couche 2) ; air-runtime reste Rust pur couche 1.

Décisions ratifiées (BDFL 2026-06-24)

  • (centrale) modèle buffers possédés + reclaim différé à l’annulation.
  • Timers via IORING_OP_TIMEOUT (zéro fd par timer).
  • spawn exige 'static (pas de scoped tasks en v1).
  • block_on non réentrant (un seul réacteur par thread).

Licence du document : MPL 2.0 Statut : v1.0 — décisions validées par le BDFL (2026-06-24), dont le modèle « buffers possédés ». Surface I/O figée sur cette base ; implémentation à suivre. Bases : ADR-023 / ADR-028 / ADR-038 / ADR-039.