Skip to main content

air_sys_syscall/io_uring/
shared.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! **Usage multi-thread** d'io_uring (Temps 3e). Sous-module
6//! `air-sys-syscall::io_uring::shared`. Repose sur l'invariant **ADR-022
7//! Décision 6** ([`IoUring`] est `Send` mais **`!Sync`**), sur `SETUP_SQPOLL`
8//! (Temps 1), `ATTACH_WQ` (Temps 3a) et `msg_ring` (Temps 2c). **Aucun register
9//! opcode propre.**
10//!
11//! Référence normative : `docs/specs/layer-0/io-uring-3e-shared.md`.
12//!
13//! ## Le point de départ : `Send` mais pas `Sync`
14//!
15//! Le protocole d'ordering du Temps 1 (§3.2) synchronise *userspace ↔ kernel*,
16//! **pas** *userspace ↔ userspace* : têtes/queues SQ/CQ et **slab S1** ne sont
17//! pas protégés contre des accès userspace concurrents. Le `!Sync` d'[`IoUring`]
18//! **interdit par typage** le partage accidentel d'un ring (prouvé par un
19//! doctest `compile_fail` sur [`LockedIoUring`]). Trois réponses, par ordre de
20//! préférence Air :
21//!
22//! 1. **Thread-per-core (§2, recommandé)** — un [`IoUring`] par thread
23//!    (`SINGLE_ISSUER | DEFER_TASKRUN`), **aucun verrou** ; communication par
24//!    `msg_ring`. Aucun type nouveau : usage direct d'[`IoUring`], déplacé
25//!    (`Send`) dans son thread. [`RingPool`] **organise** N tels rings.
26//! 2. **[`LockedIoUring`] (§3)** — un ring partagé derrière un verrou
27//!    (`Send + Sync`). Simple, mais le verrou est un **point de contention**.
28//! 3. **[`SqpollIoUring`] (§5)** — un thread kernel scrute la SQ (latence vs CPU).
29
30use super::{Completion, IoUring, IoUringBuilder, SetupFlags, SubmissionToken, raw, syscall};
31use air_sys_types::Errno;
32use air_sys_types::fd::BorrowedFd;
33use air_sys_types::io_uring::MessageRingFlags;
34use alloc::vec::Vec;
35use core::num::{NonZeroU32, NonZeroUsize};
36use core::time::Duration;
37
38use crate::sync::FutexMutex;
39
40// ───────────────────────────────────────────────────────────────────────────
41// LockedIoUring — partage simple par verrou (§3)
42// ───────────────────────────────────────────────────────────────────────────
43
44/// Un [`IoUring`] **partagé** entre threads derrière un **verrou interne**
45/// (`Send + Sync`). Le verrou sérialise les accès userspace au SQ/CQ/slab ; le
46/// protocole d'ordering userspace↔kernel du Temps 1 reste inchangé dessous.
47///
48/// **Avertissement (Principe 5)** : le verrou est un **point de contention** —
49/// sous forte charge multi-thread il sérialise tout. À réserver aux cas simples
50/// ou peu sollicités ; pour la performance, préférer le **thread-per-core** (§2,
51/// [`RingPool`]). **Verrou unique** d'abord (Principe 7) ; un affinage
52/// (soumission/complétion séparées) n'est envisagé qu'**après mesure**.
53///
54/// **Sûreté** : `Send + Sync` par construction — [`IoUring`] est `Send`, donc
55/// `FutexMutex<IoUring>` est `Send + Sync` :
56///
57/// ```
58/// fn assert_send_sync<T: Send + Sync>() {}
59/// assert_send_sync::<air_sys_syscall::io_uring::LockedIoUring>();
60/// ```
61///
62/// alors qu'un [`IoUring`] nu n'est **pas** `Sync` (partage refusé par typage) :
63///
64/// ```compile_fail
65/// fn assert_sync<T: Sync>() {}
66/// assert_sync::<air_sys_syscall::io_uring::IoUring>(); // ERREUR : IoUring: !Sync
67/// ```
68pub struct LockedIoUring {
69    /// `FutexMutex<IoUring>` : `IoUring` est `Send`, donc `FutexMutex<IoUring>`
70    /// est `Send + Sync` → `LockedIoUring` devient partageable (`Sync`). Le
71    /// verrou est un mutex futex-maison `std`-free (cf. [`crate::sync`]).
72    inner: FutexMutex<IoUring>,
73}
74
75impl core::fmt::Debug for LockedIoUring {
76    // `IoUring` n'est pas `Debug` (état de ring opaque) ; on n'affiche pas son
77    // contenu (et on ne prend pas le verrou pour formater).
78    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79        f.write_str("LockedIoUring { .. }")
80    }
81}
82
83impl LockedIoUring {
84    /// Raccourci : `LockedIoUring::from_builder(IoUringBuilder::new(entries))`.
85    ///
86    /// # Errors
87    ///
88    /// Voir [`IoUringBuilder::build`].
89    pub fn new(entries: NonZeroU32) -> Result<Self, Errno> {
90        Ok(Self {
91            inner: FutexMutex::new(IoUring::new(entries)?),
92        })
93    }
94
95    /// Construit à partir d'un [`IoUringBuilder`] (flags, SQPOLL, etc.).
96    ///
97    /// # Errors
98    ///
99    /// Voir [`IoUringBuilder::build`].
100    pub fn from_builder(builder: IoUringBuilder) -> Result<Self, Errno> {
101        Ok(Self {
102            inner: FutexMutex::new(builder.build()?),
103        })
104    }
105
106    /// Exécute `f` sur le ring sous-jacent **sous le verrou** (accès exclusif
107    /// sérialisé). C'est la primitive complète : toute opération d'[`IoUring`]
108    /// (les ~50 `submit_*`, les accesseurs de complétion) est accessible ainsi en
109    /// `&self`, sans recopier 50 forwarders. Les méthodes nommées ci-dessous en
110    /// sont de simples raccourcis pour les cas les plus fréquents.
111    ///
112    /// Le verrou est **sans empoisonnement** ([`FutexMutex`] n'implémente pas le
113    /// poisoning) : un panic d'un autre thread sous le verrou ne le rend pas
114    /// inutilisable (cohérent avec la future synchro sans poisoning d'`air-thread`).
115    pub fn with_lock<R>(&self, f: impl FnOnce(&mut IoUring) -> R) -> R {
116        let mut guard = self.inner.lock();
117        f(&mut guard)
118    }
119
120    /// `submit_read` (Temps 2a) sous le verrou.
121    ///
122    /// # Errors
123    ///
124    /// Voir [`IoUring::submit_read`].
125    pub fn submit_read(
126        &self,
127        fd: BorrowedFd<'_>,
128        buffer: Vec<u8>,
129        offset: Option<u64>,
130    ) -> Result<SubmissionToken, Errno> {
131        self.with_lock(|ring| ring.submit_read(fd, buffer, offset))
132    }
133
134    /// `submit_write` (Temps 2a) sous le verrou.
135    ///
136    /// # Errors
137    ///
138    /// Voir [`IoUring::submit_write`].
139    pub fn submit_write(
140        &self,
141        fd: BorrowedFd<'_>,
142        buffer: Vec<u8>,
143        offset: Option<u64>,
144    ) -> Result<SubmissionToken, Errno> {
145        self.with_lock(|ring| ring.submit_write(fd, buffer, offset))
146    }
147
148    /// `submit_nop` (Temps 1) sous le verrou.
149    ///
150    /// # Errors
151    ///
152    /// Voir [`IoUring::submit_nop`].
153    pub fn submit_nop(&self) -> Result<SubmissionToken, Errno> {
154        self.with_lock(|ring| ring.submit_nop())
155    }
156
157    /// `submit` (Temps 1) sous le verrou.
158    ///
159    /// # Errors
160    ///
161    /// Voir [`IoUring::submit`].
162    pub fn submit(&self) -> Result<u32, Errno> {
163        self.with_lock(|ring| ring.submit())
164    }
165
166    /// `submit_and_wait` (Temps 1) sous le verrou.
167    ///
168    /// **Attention** : l'attente bloque **avec le verrou tenu** — un seul thread
169    /// attend à la fois. Pour de l'attente concurrente, préférer le thread-per-core.
170    ///
171    /// # Errors
172    ///
173    /// Voir [`IoUring::submit_and_wait`].
174    pub fn submit_and_wait(&self, want: u32) -> Result<u32, Errno> {
175        self.with_lock(|ring| ring.submit_and_wait(want))
176    }
177
178    /// `wait_completion` (Temps 1) sous le verrou.
179    ///
180    /// # Errors
181    ///
182    /// Voir [`IoUring::wait_completion`].
183    pub fn wait_completion(&self) -> Result<Completion, Errno> {
184        self.with_lock(|ring| ring.wait_completion())
185    }
186
187    /// `try_completion` (Temps 1) sous le verrou.
188    #[must_use]
189    pub fn try_completion(&self) -> Option<Completion> {
190        self.with_lock(|ring| ring.try_completion())
191    }
192
193    /// Nombre d'opérations en vol (sous le verrou).
194    #[must_use]
195    pub fn in_flight(&self) -> u32 {
196        self.with_lock(|ring| ring.in_flight())
197    }
198}
199
200// ───────────────────────────────────────────────────────────────────────────
201// RingPool — assistant thread-per-core (§4)
202// ───────────────────────────────────────────────────────────────────────────
203
204/// Assistant **thread-per-core** : crée N [`IoUring`] cohérents en **partageant
205/// le pool io-wq** (`ATTACH_WQ`, Temps 3a) — le nombre total de threads workers
206/// kernel est **borné** quel que soit N (**crucial sur matériel modeste**, Pi 4)
207/// — et en **enregistrant les ring fds** (Temps 3a) pour le routage `msg_ring`.
208///
209/// `RingPool` **organise, ne partage pas** : chaque [`IoUring`] rendu reste
210/// `Send`/`!Sync` et est **déplacé** dans son thread, jamais partagé.
211pub struct RingPool {
212    /// Un ring possédé par worker ; le ring 0 détient le pool io-wq, les autres
213    /// s'y attachent. Ring fds enregistrés (msg_ring découplé).
214    rings: Vec<IoUring>,
215}
216
217impl core::fmt::Debug for RingPool {
218    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
219        f.debug_struct("RingPool")
220            .field("workers", &self.rings.len())
221            .finish_non_exhaustive()
222    }
223}
224
225impl RingPool {
226    /// Crée `workers` rings : le **premier** crée le pool io-wq, les **suivants**
227    /// s'y attachent (`ATTACH_WQ`) → nombre total de threads workers kernel borné.
228    /// Chaque ring fd est enregistré (`REGISTER_RING_FDS`, Temps 3a).
229    ///
230    /// # Errors
231    ///
232    /// Erreurs d'`io_uring_setup`/`io_uring_register` (cf. [`IoUringBuilder::build`],
233    /// [`IoUring::register_ring_fd`]).
234    pub fn new(workers: NonZeroUsize, entries: NonZeroU32) -> Result<Self, Errno> {
235        let count = workers.get();
236        let mut rings: Vec<IoUring> = Vec::with_capacity(count);
237        // Premier ring : crée le pool io-wq.
238        rings.push(IoUring::new(entries)?);
239        // Suivants : s'attachent au pool du premier (workers kernel bornés).
240        for _ in 1..count {
241            let attached = {
242                let first = &rings[0];
243                IoUringBuilder::new(entries)
244                    .attach_work_queue(first)
245                    .build()?
246            };
247            rings.push(attached);
248        }
249        // Enregistre chaque ring fd (routage msg_ring découplé, Temps 3a).
250        for ring in &mut rings {
251            ring.register_ring_fd()?;
252        }
253        Ok(Self { rings })
254    }
255
256    /// Distribue les rings : un [`IoUring`] **possédé** par worker, à **déplacer**
257    /// dans chaque thread.
258    #[must_use]
259    pub fn into_rings(self) -> Vec<IoUring> {
260        self.rings
261    }
262
263    /// Poignée de routage vers le ring du worker `worker` (cible `msg_ring`).
264    /// `None` si `worker` est hors borne.
265    #[must_use]
266    pub fn handle(&self, worker: usize) -> Option<RingHandle> {
267        self.rings.get(worker).map(|ring| RingHandle {
268            target_fd: ring.fd_raw(),
269        })
270    }
271}
272
273/// Référence à un ring **pair**, utilisable comme **cible de `msg_ring`**
274/// ([`IoUring::submit_message_ring_data_to`]). Porte le FD du ring pair (valide
275/// tant que ce ring vit dans son thread — l'appelant garantit sa vivacité).
276#[derive(Debug, Clone, Copy)]
277pub struct RingHandle {
278    /// FD du ring cible (process-global ; valable cross-thread).
279    target_fd: i32,
280}
281
282impl IoUring {
283    /// `msg_ring` (Temps 2c, `MSG_DATA`) ciblant un **pair** par sa [`RingHandle`]
284    /// (FD), pour réveiller/transmettre une donnée vers un ring **d'un autre
285    /// thread** (modèle thread-per-core / [`RingPool`]). Variante de
286    /// [`IoUring::submit_message_ring_data`] (qui prend une `&IoUring`, même
287    /// contexte).
288    ///
289    /// # Errors
290    ///
291    /// [`Errno::EBUSY`] si la SQ/le slab sont pleins ; à la complétion, `EBADF`
292    /// si le ring cible n'existe plus.
293    pub fn submit_message_ring_data_to(
294        &mut self,
295        target: &RingHandle,
296        res: i32,
297        user_data: u64,
298        flags: MessageRingFlags,
299    ) -> Result<SubmissionToken, Errno> {
300        let target_fd = target.target_fd;
301        let len = res.cast_unsigned();
302        let op_flags = flags.bits();
303        self.submit_op(None, |sqe| {
304            sqe.opcode = raw::IORING_OP_MSG_RING;
305            sqe.fd = target_fd;
306            sqe.addr_or_splice_off_in = raw::IORING_MSG_DATA;
307            sqe.len = len;
308            sqe.off_or_addr2 = user_data;
309            sqe.op_flags = op_flags;
310        })
311    }
312}
313
314// ───────────────────────────────────────────────────────────────────────────
315// SqpollIoUring — thread kernel de poll de la SQ (§5)
316// ───────────────────────────────────────────────────────────────────────────
317
318/// [`IoUring`] créé avec `IORING_SETUP_SQPOLL` : un **thread kernel** scrute la
319/// SQ. En régime établi, la **soumission ne fait aucun `io_uring_enter`** (juste
320/// la publication release) → latence minimale.
321///
322/// **Coût** : un **cœur kernel** dédié à scruter. Excellent sous forte charge où
323/// le CPU abonde ; **mauvais choix sur Pi 4** (cœurs rares). À réserver aux
324/// profils où la latence prime (mesure, Principe 5).
325pub struct SqpollIoUring {
326    inner: IoUring,
327}
328
329impl core::fmt::Debug for SqpollIoUring {
330    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
331        f.write_str("SqpollIoUring { .. }")
332    }
333}
334
335impl SqpollIoUring {
336    /// Crée un ring `SQPOLL`. `idle` : délai d'inactivité avant que le thread
337    /// kernel ne s'endorme (`NEED_WAKEUP`). `cpu` : épinglage optionnel du thread
338    /// (`SQ_AFF`).
339    ///
340    /// # Errors
341    ///
342    /// [`Errno::EPERM`] si `SQPOLL` n'est pas autorisé par la config ; autres
343    /// erreurs de [`IoUringBuilder::build`].
344    pub fn new(entries: NonZeroU32, idle: Duration, cpu: Option<u32>) -> Result<Self, Errno> {
345        let mut flags = SetupFlags::SQPOLL;
346        let mut builder = IoUringBuilder::new(entries);
347        if let Some(cpu) = cpu {
348            // `SQ_AFF` : épingle le thread kernel sur `cpu`.
349            flags |= SetupFlags::SQ_AFF;
350            builder = builder.with_sqpoll_cpu(cpu);
351        }
352        builder = builder.with_flags(flags).with_sqpoll_idle(idle);
353        Ok(Self {
354            inner: builder.build()?,
355        })
356    }
357
358    /// Soumet les SQE en attente. En `SQPOLL`, **aucun `io_uring_enter`** n'est
359    /// émis si le thread kernel est éveillé (il consommera la SQ) ; s'il s'est
360    /// endormi (`IORING_SQ_NEED_WAKEUP`), la façade le **réveille** automatiquement
361    /// via `io_uring_enter(.., SQ_WAKEUP)` — **seule magie cachée**, indispensable
362    /// et documentée. Retourne le nombre de SQE en attente.
363    ///
364    /// # Errors
365    ///
366    /// [`Errno::EINTR`] remonté tel quel (ADR-021 conv. 2) ; autres errno d'`enter`.
367    pub fn submit(&mut self) -> Result<u32, Errno> {
368        let to_submit = self.inner.sq.publish_and_pending();
369        if to_submit == 0 {
370            return Ok(0);
371        }
372        if self.inner.sq.flags() & raw::IORING_SQ_NEED_WAKEUP != 0 {
373            // Thread SQPOLL endormi : le réveiller.
374            // SAFETY: `fd` est un ring fd valide ; pas d'argument étendu.
375            let ret = unsafe {
376                syscall::enter(
377                    self.inner.fd_raw(),
378                    to_submit,
379                    0,
380                    raw::IORING_ENTER_SQ_WAKEUP,
381                    0,
382                    0,
383                )
384            };
385            if ret < 0 {
386                return Err(raw::errno_from_negative_syscall_ret(ret));
387            }
388            Ok(u32::try_from(ret).unwrap_or(0))
389        } else {
390            // Thread SQPOLL éveillé : il consommera la SQ, aucun syscall requis.
391            Ok(to_submit)
392        }
393    }
394
395    /// `submit_nop` (Temps 1) : prépare un SQE (sans publier — [`Self::submit`] ou
396    /// [`Self::submit_and_wait`] publie).
397    ///
398    /// # Errors
399    ///
400    /// Voir [`IoUring::submit_nop`].
401    pub fn submit_nop(&mut self) -> Result<SubmissionToken, Errno> {
402        self.inner.submit_nop()
403    }
404
405    /// `submit_read` (Temps 2a) en mode `SQPOLL`.
406    ///
407    /// # Errors
408    ///
409    /// Voir [`IoUring::submit_read`].
410    pub fn submit_read(
411        &mut self,
412        fd: BorrowedFd<'_>,
413        buffer: Vec<u8>,
414        offset: Option<u64>,
415    ) -> Result<SubmissionToken, Errno> {
416        self.inner.submit_read(fd, buffer, offset)
417    }
418
419    /// Publie (avec réveil `SQPOLL` si nécessaire) **puis** attend au moins `want`
420    /// complétions (`io_uring_enter(GETEVENTS)`). L'attente requiert un syscall
421    /// (la soumission, elle, peut n'en demander aucun).
422    ///
423    /// # Errors
424    ///
425    /// [`Errno::EINTR`] remonté tel quel ; autres errno d'`enter`.
426    pub fn submit_and_wait(&mut self, want: u32) -> Result<u32, Errno> {
427        self.submit()?;
428        if want == 0 {
429            return Ok(0);
430        }
431        // SAFETY: `fd` valide ; on attend `want` complétions, pas d'argument étendu.
432        let ret = unsafe {
433            syscall::enter(
434                self.inner.fd_raw(),
435                0,
436                want,
437                raw::IORING_ENTER_GETEVENTS,
438                0,
439                0,
440            )
441        };
442        if ret < 0 {
443            return Err(raw::errno_from_negative_syscall_ret(ret));
444        }
445        Ok(u32::try_from(ret).unwrap_or(0))
446    }
447
448    /// `wait_completion` (Temps 1).
449    ///
450    /// # Errors
451    ///
452    /// Voir [`IoUring::wait_completion`].
453    pub fn wait_completion(&mut self) -> Result<Completion, Errno> {
454        self.inner.wait_completion()
455    }
456
457    /// Nombre d'opérations en vol.
458    #[must_use]
459    pub fn in_flight(&self) -> u32 {
460        self.inner.in_flight()
461    }
462}
463
464// ───────────────────────────────────────────────────────────────────────────
465// Preuve loom — discipline de verrou de LockedIoUring (sous `cfg(loom)`)
466// ───────────────────────────────────────────────────────────────────────────
467//
468// Exécution : `RUSTFLAGS="--cfg loom" cargo test -p air-sys-syscall --lib shared::loom_proof`.
469// `LockedIoUring` enveloppe l'`IoUring` (SQ/CQ/slab non protégés) dans un `Mutex` :
470// la sûreté repose entièrement sur l'**exclusion mutuelle** du verrou. Comme un
471// `IoUring` réel n'est pas constructible sous `loom::model` (pas de syscall), on
472// modélise la **discipline** : deux threads accèdent à un état partagé gardé
473// (représentant SQ-tail/slab) UNIQUEMENT sous le verrou. loom explore tous les
474// entrelacements et prouve l'absence de course (cellule gardée) et de mise à
475// jour perdue. Si l'accès gardé fuyait hors du verrou, loom le détecterait.
476#[cfg(loom)]
477mod loom_proof {
478    use loom::cell::UnsafeCell;
479    use loom::sync::{Arc, Mutex};
480
481    /// Deux threads incrémentent un état partagé **sous le verrou** : aucune mise
482    /// à jour perdue (somme exacte), aucun accès concurrent à la cellule.
483    #[test]
484    fn mutex_serializes_shared_ring_state() {
485        loom::model(|| {
486            // `Mutex<()>` garde l'accès à la cellule (état userspace du ring).
487            let state = Arc::new((Mutex::new(()), UnsafeCell::new(0u32)));
488
489            let writer = {
490                let state = Arc::clone(&state);
491                loom::thread::spawn(move || {
492                    let (lock, cell) = &*state;
493                    let _guard = lock.lock().unwrap();
494                    // SAFETY: accès exclusif garanti par le verrou tenu ; loom
495                    // vérifie qu'aucun autre thread ne touche la cellule sans le
496                    // verrou (modèle de `LockedIoUring::with_lock`).
497                    cell.with_mut(|p| unsafe { *p += 1 });
498                })
499            };
500
501            {
502                let (lock, cell) = &*state;
503                let _guard = lock.lock().unwrap();
504                // SAFETY: idem — sous le verrou, accès exclusif à la cellule.
505                cell.with_mut(|p| unsafe { *p += 1 });
506            }
507
508            writer.join().unwrap();
509
510            let (_lock, cell) = &*state;
511            // SAFETY: les deux threads ont terminé (join) ; lecture finale exclusive.
512            let total = cell.with(|p| unsafe { *p });
513            assert_eq!(total, 2, "aucune mise à jour perdue sous le verrou");
514        });
515    }
516}
517
518// ───────────────────────────────────────────────────────────────────────────
519// Tests
520// ───────────────────────────────────────────────────────────────────────────
521//
522// Intégration kernel (io_uring non modélisé par Miri) → `#[cfg_attr(miri, ignore)]`.
523// La sûreté multi-thread est prouvée à **trois** niveaux : (1) `!Sync` d'`IoUring`
524// et `Send + Sync` de `LockedIoUring` par **doctests `compile_fail`/compile-pass**
525// (sur le type `LockedIoUring`) + assertion statique ci-dessous ; (2) **loom** sur
526// la discipline de verrou (`mod loom_proof`, sous `cfg(loom)`) ; (3) tests
527// d'intégration concurrents réels (threads).
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532    use crate::test_support::sfd;
533    use core::num::{NonZeroU32, NonZeroUsize};
534    use std::sync::Arc;
535    use std::time::Duration;
536
537    fn nz32(n: u32) -> NonZeroU32 {
538        NonZeroU32::new(n).expect("n ≠ 0")
539    }
540
541    fn nzusize(n: usize) -> NonZeroUsize {
542        NonZeroUsize::new(n).expect("n ≠ 0")
543    }
544
545    /// Assertion **statique** : `LockedIoUring` est `Send + Sync` (le `!Sync`
546    /// d'`IoUring` est prouvé par le doctest `compile_fail` sur le type).
547    #[test]
548    fn locked_io_uring_is_send_sync() {
549        fn assert_send_sync<T: Send + Sync>() {}
550        assert_send_sync::<LockedIoUring>();
551        fn assert_send<T: Send>() {}
552        assert_send::<IoUring>(); // IoUring est Send (déplaçable entre threads)
553        assert_send::<RingPool>();
554        assert_send::<SqpollIoUring>();
555    }
556
557    // ── LockedIoUring : accès concurrent multi-thread ─────────────────────
558
559    #[test]
560    #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
561    fn locked_io_uring_concurrent_nops() {
562        let ring = Arc::new(LockedIoUring::new(nz32(16)).expect("LockedIoUring"));
563        let threads: Vec<_> = (0..4)
564            .map(|_| {
565                let ring = Arc::clone(&ring);
566                std::thread::spawn(move || {
567                    // Round-trip **atomique** sous le verrou : soumission, attente
568                    // et moisson dans une **seule** section critique (`with_lock`).
569                    // Indispensable — enchaîner les raccourcis `submit_nop` /
570                    // `submit_and_wait` / `wait_completion` prend et relâche le
571                    // verrou **trois fois**, ce qui laisserait un autre thread
572                    // moissonner *ce* CQE entre-temps (course de complétion, pas
573                    // de course SQ/CQ/slab). En tenant le verrou de bout en bout,
574                    // la complétion récupérée est **forcément** la sienne.
575                    ring.with_lock(|r| {
576                        let tok = r.submit_nop().expect("nop");
577                        r.submit_and_wait(1).expect("submit_and_wait");
578                        let c = r.wait_completion().expect("completion");
579                        assert_eq!(c.token(), tok);
580                        c.completed().is_ok()
581                    })
582                })
583            })
584            .collect();
585        for t in threads {
586            assert!(t.join().expect("thread"), "nop completed");
587        }
588        assert_eq!(ring.in_flight(), 0, "aucun slot perdu");
589        // Raccourcis nommés (`&self`), en **mono-thread** : chacun prend puis
590        // relâche le verrou ; sans concurrence, le token récupéré est le sien.
591        let tok = ring.submit_nop().expect("nop via mirror");
592        ring.submit_and_wait(1).expect("submit_and_wait via mirror");
593        assert_eq!(ring.wait_completion().expect("c").token(), tok);
594        assert_eq!(ring.in_flight(), 0);
595    }
596
597    // ── RingPool : ATTACH_WQ + routage msg_ring inter-ring ────────────────
598
599    #[test]
600    #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
601    fn ring_pool_routes_msg_ring_between_peers() {
602        let pool = RingPool::new(nzusize(3), nz32(8)).expect("RingPool");
603        // Poignée vers le worker 1 (pair), capturée avant distribution.
604        let handle1 = pool.handle(1).expect("handle 1");
605        assert!(pool.handle(3).is_none(), "worker hors borne");
606        let mut rings = pool.into_rings();
607        assert_eq!(rings.len(), 3, "un ring par worker");
608        // worker 1 : attend (poll cq_ready) qu'un message arrive sur son CQ.
609        let ring1 = rings.remove(1);
610        let peer = std::thread::spawn(move || {
611            let ring1 = ring1;
612            for _ in 0..2000 {
613                if ring1.cq_ready() > 0 {
614                    return true; // message reçu (CQE posté par msg_ring du pair)
615                }
616                std::thread::sleep(Duration::from_millis(1));
617            }
618            false
619        });
620        // worker 0 : route un msg_ring vers le pair (handle 1).
621        let mut ring0 = rings.remove(0);
622        let tok = ring0
623            .submit_message_ring_data_to(&handle1, 7, 0xABCD, MessageRingFlags::empty())
624            .expect("msg_ring vers le pair");
625        // La complétion de l'op msg_ring (côté émetteur) confirme l'envoi.
626        ring0.submit_and_wait(1).expect("submit");
627        let sent = ring0.wait_completion().expect("completion émetteur");
628        assert_eq!(sent.token(), tok);
629        sent.completed().expect("msg_ring envoyé");
630        assert!(
631            peer.join().expect("thread pair"),
632            "le pair a reçu le message"
633        );
634    }
635
636    // ── SqpollIoUring : soumission + réveil NEED_WAKEUP ───────────────────
637
638    #[test]
639    #[cfg_attr(miri, ignore = "io_uring SQPOLL non supporté par Miri")]
640    fn sqpoll_submits_and_wakes_after_idle() {
641        // idle court (10 ms) pour exercer le réveil NEED_WAKEUP après inactivité.
642        let mut ring = match SqpollIoUring::new(nz32(8), Duration::from_millis(10), None) {
643            Ok(r) => r,
644            // SQPOLL peut être refusé selon la config (privilège/cgroup) → on signale.
645            Err(e) => {
646                assert!(
647                    matches!(e, Errno::EPERM),
648                    "SQPOLL indisponible : {e:?} (attendu EPERM si refusé)"
649                );
650                return;
651            }
652        };
653        // 1ère soumission : thread SQPOLL éveillé (vient d'être créé) → pas de syscall.
654        let tok = ring.submit_nop().expect("nop");
655        ring.submit()
656            .expect("submit (SQPOLL, sans enter en régime établi)");
657        ring.submit_and_wait(1).expect("submit_and_wait");
658        let c = ring.wait_completion().expect("completion");
659        assert_eq!(c.token(), tok);
660        c.completed().expect("nop ok");
661        assert_eq!(ring.in_flight(), 0);
662        // Laisse le thread SQPOLL s'endormir (> idle), puis re-soumets : la façade
663        // doit détecter NEED_WAKEUP et réveiller le thread (chemin SQ_WAKEUP).
664        std::thread::sleep(Duration::from_millis(60));
665        let tok2 = ring.submit_nop().expect("nop 2");
666        // submit() après idle : NEED_WAKEUP détecté → réveil (chemin SQ_WAKEUP).
667        ring.submit().expect("submit (réveil NEED_WAKEUP)");
668        ring.submit_and_wait(1).expect("submit_and_wait après idle");
669        let c2 = ring.wait_completion().expect("completion 2");
670        assert_eq!(c2.token(), tok2);
671        c2.completed().expect("nop 2 ok (réveil NEED_WAKEUP)");
672        // Le thread SQPOLL est maintenant **éveillé** : une rafale de soumissions
673        // immédiates exerce le chemin **sans syscall** (`!NEED_WAKEUP`).
674        for _ in 0..16 {
675            let tok = ring.submit_nop().expect("nop rafale");
676            ring.submit().expect("submit éveillé (sans enter)");
677            ring.submit_and_wait(1).expect("drain");
678            assert_eq!(ring.wait_completion().expect("c").token(), tok);
679        }
680    }
681
682    #[test]
683    #[cfg_attr(miri, ignore = "io_uring SQPOLL non supporté par Miri")]
684    fn sqpoll_with_cpu_affinity_round_trips() {
685        // SQ_AFF : épingle le thread SQPOLL sur le CPU 0 (présent sur tout hôte).
686        let mut ring = match SqpollIoUring::new(nz32(8), Duration::from_millis(100), Some(0)) {
687            Ok(r) => r,
688            Err(e) => {
689                assert!(matches!(e, Errno::EPERM), "SQPOLL/SQ_AFF refusé : {e:?}");
690                return;
691            }
692        };
693        let tok = ring.submit_nop().expect("nop");
694        let n = ring.submit_and_wait(1).expect("submit_and_wait");
695        assert!(n <= 1);
696        assert_eq!(ring.wait_completion().expect("c").token(), tok);
697        assert_eq!(ring.in_flight(), 0);
698    }
699
700    // ── Thread-per-core : un ring par thread (modèle §2) ──────────────────
701
702    #[test]
703    #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
704    fn thread_per_core_each_thread_owns_its_ring() {
705        // 3 threads, un IoUring déplacé (Send) dans chacun, charge indépendante,
706        // aucun partage ⇒ aucun verrou.
707        let threads: Vec<_> = (0..3)
708            .map(|_| {
709                std::thread::spawn(|| {
710                    let mut ring = IoUring::new(nz32(8)).expect("ring");
711                    let tok = ring.submit_nop().expect("nop");
712                    ring.submit_and_wait(1).expect("submit");
713                    let c = ring.wait_completion().expect("completion");
714                    assert_eq!(c.token(), tok);
715                    c.completed().is_ok()
716                })
717            })
718            .collect();
719        for t in threads {
720            assert!(t.join().expect("thread"), "ring indépendant OK");
721        }
722    }
723
724    // ── Couverture : mirrors LockedIoUring + read SQPOLL + Debug ──────────
725
726    #[test]
727    #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
728    fn locked_io_uring_read_write_and_debug() {
729        let path = std::env::temp_dir().join(format!("air-iou3e-{}", std::process::id()));
730        {
731            std::fs::File::create(&path).expect("create");
732        }
733        let file = std::fs::OpenOptions::new()
734            .read(true)
735            .write(true)
736            .open(&path)
737            .expect("open rw");
738        // `from_builder` (couvre la construction via builder).
739        let ring = LockedIoUring::from_builder(IoUringBuilder::new(nz32(8)))
740            .expect("LockedIoUring from_builder");
741        // Debug ne prend pas le verrou et n'affiche pas l'état du ring.
742        assert_eq!(format!("{ring:?}"), "LockedIoUring { .. }");
743        // submit_write puis submit() explicite (mirror) puis wait.
744        ring.submit_write(sfd(&file), b"shared".to_vec(), Some(0))
745            .expect("submit_write");
746        assert!(ring.submit().expect("submit") >= 1);
747        let (_buf, n) = ring
748            .wait_completion()
749            .expect("completion")
750            .into_buffer_result()
751            .expect("write ok");
752        assert_eq!(n, 6);
753        // submit_read (mirror) + try_completion.
754        let tok = ring
755            .submit_read(sfd(&file), vec![0u8; 6], Some(0))
756            .expect("submit_read");
757        ring.submit_and_wait(1).expect("submit_and_wait");
758        // try_completion (mirror) récupère la complétion prête.
759        let c = ring.try_completion().expect("try_completion prête");
760        assert_eq!(c.token(), tok);
761        let (buf, n) = c.into_buffer_result().expect("read ok");
762        assert_eq!(n, 6);
763        assert_eq!(&buf, b"shared");
764        assert_eq!(ring.in_flight(), 0);
765        std::fs::remove_file(&path).ok();
766    }
767
768    #[test]
769    #[cfg_attr(miri, ignore = "io_uring non supporté par Miri")]
770    fn ring_pool_and_sqpoll_debug_and_extras() {
771        let pool = RingPool::new(nzusize(2), nz32(8)).expect("pool");
772        assert!(format!("{pool:?}").contains("RingPool"));
773        assert!(format!("{pool:?}").contains('2'), "workers affichés");
774        drop(pool.into_rings());
775        // SQPOLL : Debug + submit_read + submit_and_wait(0) (branche want == 0).
776        let mut sq = match SqpollIoUring::new(nz32(8), Duration::from_millis(100), None) {
777            Ok(r) => r,
778            Err(e) => {
779                assert!(matches!(e, Errno::EPERM), "SQPOLL refusé : {e:?}");
780                return;
781            }
782        };
783        assert_eq!(format!("{sq:?}"), "SqpollIoUring { .. }");
784        // submit_and_wait(0) : publie (sans rien) puis retourne sans attendre.
785        assert_eq!(sq.submit_and_wait(0).expect("submit_and_wait(0)"), 0);
786        // submit_read sur un fichier.
787        use std::io::Write as _;
788        let path = std::env::temp_dir().join(format!("air-iou3e-sq-{}", std::process::id()));
789        {
790            let mut f = std::fs::File::create(&path).expect("create");
791            f.write_all(b"sqpoll").expect("write");
792        }
793        let file = std::fs::File::open(&path).expect("open");
794        let tok = sq
795            .submit_read(sfd(&file), vec![0u8; 6], Some(0))
796            .expect("submit_read SQPOLL");
797        sq.submit_and_wait(1).expect("submit_and_wait");
798        let completion = sq.wait_completion().expect("completion");
799        assert_eq!(completion.token(), tok);
800        let (buf, n) = completion.into_buffer_result().expect("read ok");
801        assert_eq!(n, 6);
802        assert_eq!(&buf, b"sqpoll");
803        assert_eq!(sq.in_flight(), 0);
804        std::fs::remove_file(&path).ok();
805    }
806
807    // ── SQPOLL : bras d'erreur d'`io_uring_enter` (simulateur) ────────────
808    //
809    // Les deux `io_uring_enter` de `SqpollIoUring` — réveil `SQ_WAKEUP`
810    // (`submit`) et attente `GETEVENTS` (`submit_and_wait`) — passent par la
811    // **même couture instrumentable** `syscall::enter` que les autres syscalls
812    // io_uring (cf. `registration.rs`). Le simulateur y injecte un `-errno` :
813    // l'errno transite par le **même** `errno_from_negative_syscall_ret` que le
814    // vrai kernel et doit **remonter tel quel** (ADR-021 conv. 2 : `EINTR` non
815    // retenté). On couvre ainsi les bras d'erreur de façon **déterministe**.
816
817    #[test]
818    #[cfg_attr(miri, ignore = "io_uring SQPOLL non supporté par Miri")]
819    fn sqpoll_submit_propagates_enter_error_on_wakeup() {
820        use super::syscall::sim::{self, Syscall};
821        sim::clear();
822        let mut ring = match SqpollIoUring::new(nz32(8), Duration::from_millis(10), None) {
823            Ok(r) => r,
824            Err(e) => {
825                assert!(matches!(e, Errno::EPERM), "SQPOLL refusé : {e:?}");
826                return;
827            }
828        };
829        // Prépare un SQE **sans publier**, puis laisse le thread kernel SQPOLL
830        // s'endormir : `idle` vaut 10 ms et on attend bien plus — il pose alors
831        // `NEED_WAKEUP` sur la SQ. Délai **éprouvé**, identique à
832        // `sqpoll_submits_and_wakes_after_idle` (vert en CI sur les deux arches).
833        // `submit` empruntera donc le bras `SQ_WAKEUP` → `enter`. Pas de boucle
834        // conditionnelle (qui laisserait un corps non exécuté si `NEED_WAKEUP`
835        // était déjà posé) : si la précondition n'était pas tenue, le `submit`
836        // ci-dessous renverrait `Ok` et l'`expect_err` échouerait bruyamment.
837        let _token = ring.submit_nop().expect("nop");
838        std::thread::sleep(Duration::from_millis(60));
839        // Le simulateur fait échouer l'`enter(SQ_WAKEUP)` ⇒ l'errno doit remonter.
840        sim::inject(Syscall::Enter, -4); // -EINTR
841        let err = ring.submit().expect_err("enter SQ_WAKEUP en échec → Err");
842        assert!(
843            matches!(err, Errno::EINTR),
844            "EINTR remonté tel quel (conv. 2 ADR-021) : {err:?}"
845        );
846        sim::clear();
847    }
848
849    #[test]
850    #[cfg_attr(miri, ignore = "io_uring SQPOLL non supporté par Miri")]
851    fn sqpoll_submit_and_wait_propagates_enter_error_on_getevents() {
852        use super::syscall::sim::{self, Syscall};
853        sim::clear();
854        let mut ring = match SqpollIoUring::new(nz32(8), Duration::from_millis(100), None) {
855            Ok(r) => r,
856            Err(e) => {
857                assert!(matches!(e, Errno::EPERM), "SQPOLL refusé : {e:?}");
858                return;
859            }
860        };
861        // **Aucun** SQE en attente : `submit` (appelé en tête de
862        // `submit_and_wait`) court-circuite sur `to_submit == 0` (aucun
863        // `enter`). Le **seul** `enter` émis est donc le `GETEVENTS` d'attente
864        // → le simulateur le fait échouer et l'errno doit remonter.
865        // Entièrement déterministe (aucune dépendance au timing du thread).
866        sim::inject(Syscall::Enter, -4); // -EINTR
867        let err = ring
868            .submit_and_wait(1)
869            .expect_err("enter GETEVENTS en échec → Err");
870        assert!(
871            matches!(err, Errno::EINTR),
872            "EINTR remonté tel quel : {err:?}"
873        );
874        sim::clear();
875    }
876}