Skip to main content

pal_uring/
threadpool.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! This module implements the following functionality:
5//! - A threadpool that executes tasks on a pool of affinitized worker threads, and manages a pool of
6//!   `IO-Uring`s used to execute asynchronous I/Os. Clients of the threadpool can initiate I/Os on any
7//!   of the rings, but each worker thread owns one `IO-Uring` instance and processes all completions
8//!   for that instance.
9//! - An async task executor that starts a task on the current thread, and then polls it to completion
10//!   on an a worker affinitized to the same processor where the task started. The use case for this
11//!   executor is the VSCL, where we intend to start an I/O processing task on the VP run thread that
12//!   handles a VMBUS interrupt from VTL0, and then later process the I/O completion on the same processor
13//!   (either on an affinitized worker thread, or possibly on the VP run thread itself).
14//! - A future that represents an async I/O request issued via the IO-Uring mechanism.
15
16use super::ioring::IoCompletionRing;
17use super::ioring::IoMemory;
18use super::ioring::IoRing;
19use futures::FutureExt;
20use inspect::Inspect;
21use io_uring::opcode;
22use io_uring::squeue;
23use loan_cell::LoanCell;
24use pal_async::task::Runnable;
25use pal_async::task::Schedule;
26use pal_async::task::Scheduler;
27use pal_async::task::Spawn;
28use pal_async::task::TaskMetadata;
29use pal_async::task::TaskQueue;
30use std::borrow::Borrow;
31use std::cell::Cell;
32use std::cell::RefCell;
33use std::fmt::Debug;
34use std::future::Future;
35use std::future::poll_fn;
36use std::io;
37use std::os::unix::prelude::*;
38use std::pin::Pin;
39use std::pin::pin;
40use std::process::abort;
41use std::sync::Arc;
42use std::task::Context;
43use std::task::Poll;
44use std::task::Wake;
45use std::task::Waker;
46
47/// An io-uring backed pool of tasks and IO.
48pub struct IoUringPool {
49    client: PoolClient,
50    worker: Arc<Worker>,
51    completion_ring: IoCompletionRing,
52    queue: TaskQueue,
53}
54
55impl IoUringPool {
56    /// Builds a new pool with the given ring size. `name` is used as the name of the executor.
57    pub fn new(name: impl Into<Arc<str>>, ring_size: u32) -> io::Result<Self> {
58        let (queue, scheduler) = pal_async::task::task_queue(name);
59
60        let (io_ring, completion_ring) = IoRing::new(ring_size)?;
61        let worker = Arc::new(Worker::new(ring_size, io_ring));
62
63        Ok(Self {
64            client: PoolClient(worker.clone().initiator(scheduler)),
65            worker,
66            completion_ring,
67            queue,
68        })
69    }
70
71    /// Returns the client used to configure the pool and get the initiator.
72    pub fn client(&self) -> &PoolClient {
73        &self.client
74    }
75
76    /// Runs the pool until all clients have been dropped and any registered idle
77    /// tasks have completed.
78    pub fn run(mut self) {
79        drop(self.client);
80        self.worker.run(self.completion_ring, self.queue.run())
81    }
82}
83
84/// A client for manipulating a running [`IoUringPool`].
85#[derive(Debug, Clone, Inspect)]
86#[inspect(transparent)]
87pub struct PoolClient(#[inspect(with = "|x| &x.client")] IoInitiator);
88
89impl PoolClient {
90    /// Sets the idle task to run. The task is returned by `f`, which receives
91    /// the file descriptor of the IO ring.
92    ///
93    /// The idle task is run before waiting on the IO ring. The idle task can
94    /// block synchronously by first calling [`IdleControl::pre_block`], and
95    /// then by polling on the IO ring while the task blocks.
96    //
97    // TODO: move this functionality into underhill_threadpool.
98    pub fn set_idle_task<F>(&self, f: F)
99    where
100        F: 'static + Send + AsyncFnOnce(IdleControl),
101    {
102        // Keep the pool alive as long as the idle task is running by keeping a
103        // clone of this client.
104        let keep_pool_alive = self.clone();
105        let f = Box::new(|fd| {
106            Box::pin(async move {
107                let _keep_pool_alive = keep_pool_alive;
108                f(fd).await
109            }) as Pin<Box<dyn Future<Output = _>>>
110        })
111            as Box<dyn Send + FnOnce(IdleControl) -> Pin<Box<dyn Future<Output = ()>>>>;
112
113        // Spawn a short-lived task to update the idle task.
114        let worker_id = Arc::as_ptr(&self.0.client.worker) as usize; // cast because pointers are not Send
115        let task = self.0.spawn("set_idle_task", async move {
116            THREADPOOL_WORKER_STATE.with(|state| {
117                state.borrow(|state| {
118                    let state = state.unwrap();
119                    assert_eq!(Arc::as_ptr(&state.worker), worker_id as *const _);
120                    state.new_idle_task.set(Some(f));
121                })
122            })
123        });
124
125        task.detach();
126    }
127
128    /// Returns the IO initiator.
129    pub fn initiator(&self) -> &IoInitiator {
130        &self.0
131    }
132
133    /// Sets the CPU affinity for the kernel io-uring worker threads.
134    pub fn set_iowq_affinity(&self, affinity: &pal::unix::affinity::CpuSet) -> io::Result<()> {
135        self.0.client.worker.io_ring.set_iowq_affinity(affinity)
136    }
137
138    /// Sets the maximum bounded and unbounded workers (per NUMA node) for the
139    /// ring.
140    pub fn set_iowq_max_workers(
141        &self,
142        bounded: Option<u32>,
143        unbounded: Option<u32>,
144    ) -> io::Result<()> {
145        self.0
146            .client
147            .worker
148            .io_ring
149            .set_iowq_max_workers(bounded, unbounded)
150    }
151}
152
153impl Schedule for PoolClient {
154    fn schedule(&self, runnable: Runnable) {
155        self.0.client.schedule(runnable)
156    }
157
158    fn name(&self) -> Arc<str> {
159        self.0.client.name()
160    }
161}
162
163#[derive(Debug, inspect::Inspect)]
164pub(crate) struct Worker {
165    io_ring_size: u32,
166    io_ring: IoRing,
167}
168
169type IdleTask = Pin<Box<dyn Future<Output = ()>>>;
170type IdleTaskSpawn = Box<dyn Send + FnOnce(IdleControl) -> IdleTask>;
171
172struct AffinitizedWorkerState {
173    worker: Arc<Worker>,
174    wake: Cell<bool>,
175    new_idle_task: Cell<Option<IdleTaskSpawn>>,
176    completion_ring: RefCell<IoCompletionRing>,
177}
178
179thread_local! {
180    static THREADPOOL_WORKER_STATE: LoanCell<AffinitizedWorkerState> = const { LoanCell::new() };
181}
182
183impl Wake for Worker {
184    fn wake_by_ref(self: &Arc<Self>) {
185        THREADPOOL_WORKER_STATE.with(|state| {
186            state.borrow(|state| {
187                if let Some(state) = state {
188                    if Arc::ptr_eq(self, &state.worker) {
189                        state.wake.set(true);
190                        return;
191                    }
192                }
193                // Submit a nop request to wake up the worker.
194                //
195                // SAFETY: nop opcode does not reference any data.
196                unsafe {
197                    self.io_ring.push(opcode::Nop::new().build(), true);
198                }
199            })
200        })
201    }
202
203    fn wake(self: Arc<Self>) {
204        self.wake_by_ref()
205    }
206}
207
208impl Worker {
209    pub fn new(io_ring_size: u32, io_ring: IoRing) -> Self {
210        Self {
211            io_ring_size,
212            io_ring,
213        }
214    }
215
216    pub fn initiator(self: Arc<Self>, scheduler: Scheduler) -> IoInitiator {
217        IoInitiator {
218            client: Arc::new(WorkerClient {
219                scheduler,
220                worker: self,
221            }),
222        }
223    }
224
225    pub fn run<Fut: Future>(
226        self: Arc<Self>,
227        completion_ring: IoCompletionRing,
228        fut: Fut,
229    ) -> Fut::Output {
230        tracing::debug!(
231            io_ring_size = self.io_ring_size,
232            "AffinitizedWorker running"
233        );
234
235        let waker = self.clone().into();
236        let mut cx = Context::from_waker(&waker);
237        let mut fut = pin!(fut);
238        let mut idle_task = None;
239        let state = AffinitizedWorkerState {
240            worker: self,
241            wake: Cell::new(false),
242            new_idle_task: Cell::new(None),
243            completion_ring: RefCell::new(completion_ring),
244        };
245
246        THREADPOOL_WORKER_STATE.with(|slot| {
247            slot.lend(&state, || {
248                loop {
249                    // Wake tasks due to IO completion.
250                    state.completion_ring.borrow_mut().process();
251
252                    match fut.poll_unpin(&mut cx) {
253                        Poll::Ready(r) => {
254                            tracing::debug!("AffinitizedWorker exiting");
255                            break r;
256                        }
257                        Poll::Pending => {}
258                    }
259
260                    if !state.wake.take() {
261                        if let Some(new_idle_task) = state.new_idle_task.take() {
262                            idle_task = Some(new_idle_task(IdleControl {
263                                inner: state.worker.clone(),
264                            }));
265                            tracing::debug!("new idle task");
266                        }
267
268                        if let Some(task) = &mut idle_task {
269                            match task.poll_unpin(&mut cx) {
270                                Poll::Ready(()) => {
271                                    tracing::debug!("idle task done");
272                                    idle_task = None;
273                                }
274                                Poll::Pending => {}
275                            }
276
277                            if state.wake.take() {
278                                continue;
279                            }
280                        }
281
282                        // About to block in io_uring_enter. Mark this thread
283                        // as quiesced for the global RCU domain so that any
284                        // concurrent `synchronize_blocking()` writer (e.g.
285                        // `guestmem::rcu()` page-protection updates in
286                        // OpenHCL's `underhill_mem`) can complete without
287                        // issuing a process-wide `membarrier()` on our
288                        // behalf. Without this, every worker that has ever
289                        // polled a future containing a `guestmem` critical
290                        // section stays registered as a non-quiesced RCU
291                        // reader for the lifetime of the thread, forcing
292                        // each writer to broadcast `membarrier(PRIVATE_
293                        // EXPEDITED)` to every CPU. On large isolated VMs
294                        // (e.g. 64-VP) that broadcast can stall long
295                        // enough to trigger kernel `rcu_preempt self-
296                        // detected stall` warnings in
297                        // `smp_call_function_many_cond`. Re-entering a
298                        // critical section after this issues a local
299                        // memory barrier via `ThreadData::enter_slow`, so
300                        // correctness is preserved.
301                        minircu::global().quiesce();
302
303                        state.worker.io_ring.submit_and_wait();
304                    }
305                }
306            })
307        })
308    }
309}
310
311/// Control interface used by the idle task.
312#[derive(Debug)]
313pub struct IdleControl {
314    inner: Arc<Worker>,
315}
316
317impl IdleControl {
318    /// Call before blocking in the idle task.
319    ///
320    /// Returns true if it is OK to block. Returns false if the idle task should
321    /// immediately yield instead of blocking.
322    pub fn pre_block(&mut self) -> bool {
323        THREADPOOL_WORKER_STATE.with(|state| {
324            state.borrow(|state| {
325                let state = state.unwrap();
326                assert!(Arc::ptr_eq(&state.worker, &self.inner));
327
328                // Issue IOs.
329                //
330                // FUTURE: get the idle task to do this. This will require an
331                // io-uring change to allow other drivers to call submit.
332                self.inner.io_ring.submit();
333                // If the thread was woken or there are completed IOs, ask the idle
334                // task to yield.
335                !state.wake.get() && state.completion_ring.borrow().is_empty()
336            })
337        })
338    }
339
340    /// The file descriptor of the IO ring.
341    ///
342    /// The idle task should poll on this fd while blocking.
343    pub fn ring_fd(&self) -> BorrowedFd<'_> {
344        self.inner.io_ring.as_fd()
345    }
346}
347
348impl Spawn for IoInitiator {
349    fn scheduler(&self, _metadata: &TaskMetadata) -> Arc<dyn Schedule> {
350        self.client.clone()
351    }
352}
353
354#[derive(Debug, inspect::Inspect)]
355struct WorkerClient {
356    #[inspect(skip)]
357    scheduler: Scheduler,
358    #[inspect(flatten)]
359    worker: Arc<Worker>,
360}
361
362impl Schedule for WorkerClient {
363    fn schedule(&self, runnable: Runnable) {
364        self.scheduler.schedule(runnable)
365    }
366
367    fn name(&self) -> Arc<str> {
368        self.scheduler.name()
369    }
370}
371
372/// Client handle for initiating IOs or spawning tasks on a specific threadpool
373/// thread.
374#[derive(Debug, Clone)]
375pub struct IoInitiator {
376    client: Arc<WorkerClient>,
377}
378
379impl IoInitiator {
380    /// Probes the ring for supporting a given opcode.
381    pub fn probe(&self, opcode: u8) -> bool {
382        self.client.worker.io_ring.probe(opcode)
383    }
384
385    /// Issues an IO described by `f`, referencing IO memory in `io_mem`.
386    ///
387    /// The submission queue entry for the IO is provided by `f` so that the IO
388    /// can reference memory in the `io_mem` object. A reference to `io_mem` is
389    /// passed to `f` after it has been pinned in memory so that it will not
390    /// move for the lifetime of the IO.
391    ///
392    /// Once the IO has completed, both the result and the IO memory are
393    /// returned.
394    ///
395    /// # Safety
396    ///
397    /// The caller must guarantee that `f` returns a submission queue entry that
398    /// only references memory of static lifetime or that is part of the
399    /// `io_mem` object passed to `f`.
400    ///
401    /// # Aborts
402    ///
403    /// The process will abort if the async function is dropped before it
404    /// completes. This is because the IO memory is not moved into the heap, and
405    /// `drop` cannot synchronously wait for the IO to complete.
406    pub async unsafe fn issue_io<T, F>(&self, mut io_mem: T, f: F) -> (io::Result<i32>, T)
407    where
408        T: 'static + Unpin,
409        F: FnOnce(&mut T) -> squeue::Entry,
410    {
411        // Note that this function is written carefully to minimize the
412        // generated future size.
413
414        struct AbortOnDrop;
415
416        impl Drop for AbortOnDrop {
417            fn drop(&mut self) {
418                eprintln!("io dropped in flight, may reference stack memory, aborting process");
419                abort();
420            }
421        }
422
423        // Abort if this future is dropped while the IO is in flight.
424        let abort_on_drop = AbortOnDrop;
425
426        // Initiate and wait for the IO.
427        let result = poll_fn({
428            enum State<F> {
429                NotIssued(F),
430                Issued(usize),
431                Invalid,
432            }
433
434            let mut state = State::NotIssued(f);
435            let io_mem = &mut io_mem;
436            move |cx: &mut Context<'_>| {
437                match std::mem::replace(&mut state, State::Invalid) {
438                    State::NotIssued(f) => {
439                        // SAFETY: validity of the entry is guaranteed by the caller.
440                        state = State::Issued(unsafe {
441                            self.submit_io((f)(io_mem), IoMemory::new(()), cx.waker().clone())
442                        });
443
444                        // Wait once until the waker is woken.
445                        Poll::Pending
446                    }
447                    State::Issued(idx) => match self.poll_io(cx, idx) {
448                        Poll::Ready((result, _)) => Poll::Ready(result),
449                        Poll::Pending => {
450                            state = State::Issued(idx);
451                            Poll::Pending
452                        }
453                    },
454                    State::Invalid => unreachable!(),
455                }
456            }
457        })
458        .await;
459
460        // The IO is complete, so io_mem is no longer aliased.
461        std::mem::forget(abort_on_drop);
462
463        let result = if result >= 0 {
464            Ok(result)
465        } else {
466            Err(io::Error::from_raw_os_error(-result))
467        };
468
469        (result, io_mem)
470    }
471
472    /// # Safety
473    ///
474    /// The caller must guarantee that the given io_mem is compatible with the given sqe.
475    unsafe fn submit_io(&self, sqe: squeue::Entry, io_mem: IoMemory, waker: Waker) -> usize {
476        // Only submit if the worker is not currently running on this thread--if it is, the
477        // IO will be submitted soon.
478        let needs_submit = THREADPOOL_WORKER_STATE.with(|state| {
479            state.borrow(|state| {
480                state.is_none_or(|state| !Arc::ptr_eq(&state.worker, &self.client.worker))
481            })
482        });
483
484        // SAFETY: caller guarantees sqe and io_mem are compatible.
485        unsafe {
486            self.client
487                .worker
488                .io_ring
489                .new_io(sqe, io_mem, waker, needs_submit)
490        }
491    }
492
493    fn poll_io(&self, cx: &mut Context<'_>, idx: usize) -> Poll<(i32, IoMemory)> {
494        self.client.worker.io_ring.poll_io(cx, idx)
495    }
496
497    fn drop_io(&self, idx: usize) {
498        self.client.worker.io_ring.drop_io(idx);
499    }
500}
501
502/// A future representing an IO request submitted to an `IoRingPool`.
503pub struct Io<T, Init: Borrow<IoInitiator> = IoInitiator> {
504    initiator: Init,
505    state: IoState<T>,
506}
507
508enum IoState<T> {
509    NotStarted(squeue::Entry, T),
510    Started(usize),
511    Completed(i32, T),
512    Invalid,
513}
514
515impl<T, Init: Borrow<IoInitiator>> Debug for Io<T, Init> {
516    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517        f.pad("Io")
518    }
519}
520
521impl<T: 'static + Send + Sync + Unpin, Init: Borrow<IoInitiator> + Unpin> Io<T, Init> {
522    /// Creates a new request that will submit the IO described by the submission queue entry
523    /// to the specified initiator.
524    ///
525    /// # Safety
526    ///
527    /// The caller must guarantee that the `submission_queue_entry` only references memory
528    /// owned by the supplied `io_mem` object.
529    pub unsafe fn new(initiator: Init, submission_queue_entry: squeue::Entry, io_mem: T) -> Self {
530        Self {
531            initiator,
532            state: IoState::NotStarted(submission_queue_entry, io_mem),
533        }
534    }
535
536    /// Returns the initiator used to issue the IO.
537    pub fn initiator(&self) -> &Init {
538        &self.initiator
539    }
540
541    /// Issues an async cancel operation for this IO.
542    pub fn cancel(&self) {
543        // SAFETY: the AsyncCancel entry does not reference any external memory.
544        unsafe {
545            self.cancel_inner(|user_data| opcode::AsyncCancel::new(user_data).build());
546        }
547    }
548
549    /// Issues a timeout remove operation for this IO.
550    pub fn cancel_timeout(&self) {
551        // SAFETY: the TimeoutRemove entry does not reference any external memory.
552        unsafe {
553            self.cancel_inner(|user_data| opcode::TimeoutRemove::new(user_data).build());
554        }
555    }
556
557    /// Issues a poll remove operation for this IO.
558    pub fn cancel_poll(&self) {
559        // SAFETY: the PollRemove entry does not reference any external memory.
560        unsafe {
561            self.cancel_inner(|user_data| opcode::PollRemove::new(user_data).build());
562        }
563    }
564
565    /// # Safety
566    ///
567    /// Caller must ensure that `f` produces a safe sqe entry.
568    unsafe fn cancel_inner(&self, f: impl FnOnce(u64) -> squeue::Entry) {
569        let sqe = f(self.user_data().unwrap());
570        // SAFETY: guaranteed by caller
571        let idx = unsafe {
572            self.initiator
573                .borrow()
574                .submit_io(sqe, IoMemory::new(()), Waker::noop().clone())
575        };
576        self.initiator.borrow().drop_io(idx);
577    }
578
579    /// Retrieves the IO memory.
580    ///
581    /// Panics if the IO has started and has not yet completed.
582    pub fn into_mem(mut self) -> T {
583        match std::mem::replace(&mut self.state, IoState::Invalid) {
584            IoState::Started(_) => {
585                panic!("io is not complete");
586            }
587            IoState::NotStarted(_, io_mem) | IoState::Completed(_, io_mem) => io_mem,
588            IoState::Invalid => unreachable!(),
589        }
590    }
591
592    /// Returns the `user_data` field used when intiating the IO, or `None` if
593    /// the IO has not yet been initiated. This is necessary to support
594    /// cancelling IOs.
595    pub fn user_data(&self) -> Option<u64> {
596        match self.state {
597            IoState::Started(idx) => Some(idx as u64),
598            IoState::NotStarted(_, _) | IoState::Completed(_, _) | IoState::Invalid => None,
599        }
600    }
601}
602
603impl<T: 'static + Sync + Send + Unpin, Init: Borrow<IoInitiator> + Unpin> Future for Io<T, Init> {
604    type Output = io::Result<i32>;
605
606    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
607        let this = Pin::get_mut(self);
608        let result = match std::mem::replace(&mut this.state, IoState::Invalid) {
609            IoState::NotStarted(entry, io_mem) => {
610                // SAFETY: guaranteed by unsafe Self::new.
611                let idx = unsafe {
612                    this.initiator.borrow().submit_io(
613                        entry,
614                        IoMemory::new(io_mem),
615                        cx.waker().clone(),
616                    )
617                };
618                this.state = IoState::Started(idx);
619                return Poll::Pending;
620            }
621            IoState::Started(idx) => {
622                this.state = IoState::Started(idx);
623                let (result, io_mem) = std::task::ready!(this.initiator.borrow().poll_io(cx, idx));
624                this.state = IoState::Completed(result, io_mem.downcast());
625                result
626            }
627            IoState::Completed(result, io_mem) => {
628                this.state = IoState::Completed(result, io_mem);
629                result
630            }
631            IoState::Invalid => unreachable!(),
632        };
633        let result = if result >= 0 {
634            Ok(result)
635        } else {
636            Err(io::Error::from_raw_os_error(-result))
637        };
638        Poll::Ready(result)
639    }
640}
641
642impl<T, Init: Borrow<IoInitiator>> Drop for Io<T, Init> {
643    fn drop(&mut self) {
644        if let IoState::Started(idx) = self.state {
645            self.initiator.borrow().drop_io(idx);
646        }
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    #![expect(
653        clippy::disallowed_methods,
654        reason = "test code using futures channels"
655    )]
656
657    use super::Io;
658    use super::IoRing;
659    use crate::IoUringPool;
660    use crate::uring::tests::SingleThreadPool;
661    use futures::executor::block_on;
662    use io_uring::opcode;
663    use io_uring::types;
664    use pal_async::task::Spawn;
665    use parking_lot::Mutex;
666    use std::future::Future;
667    use std::os::unix::prelude::*;
668    use std::pin::Pin;
669    use std::sync::Arc;
670    use std::sync::atomic::AtomicBool;
671    use std::sync::atomic::Ordering;
672    use std::task::Context;
673    use std::task::Poll;
674    use std::task::Waker;
675    use std::thread;
676    use std::time::Duration;
677    use std::time::Instant;
678    use tempfile::NamedTempFile;
679    use test_with_tracing::test;
680
681    const PAGE_SIZE: usize = 4096;
682
683    fn new_test_file() -> NamedTempFile {
684        let mut file = NamedTempFile::new().unwrap();
685        file.as_file_mut().set_len(1024 * 64).unwrap();
686        file
687    }
688
689    struct Env {
690        rx: std::sync::mpsc::Receiver<()>,
691    }
692
693    struct TestCase {
694        tp: SingleThreadPool,
695        file: NamedTempFile,
696        _tx: std::sync::mpsc::Sender<()>,
697    }
698
699    impl Drop for Env {
700        fn drop(&mut self) {
701            while self.rx.recv().is_ok() {}
702        }
703    }
704
705    fn new_test() -> (Env, TestCase) {
706        let file = new_test_file();
707        let tp = SingleThreadPool::new().unwrap();
708        let (tx, rx) = std::sync::mpsc::channel();
709        (Env { rx }, TestCase { tp, file, _tx: tx })
710    }
711
712    struct Timeout {
713        shared_state: Arc<Mutex<TimeoutState>>,
714    }
715
716    struct TimeoutState {
717        completed: bool,
718        waker: Option<Waker>,
719    }
720
721    impl Future for Timeout {
722        type Output = ();
723
724        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
725            let mut shared_state = self.shared_state.lock();
726            if shared_state.completed {
727                Poll::Ready(())
728            } else {
729                shared_state.waker = Some(cx.waker().clone());
730                Poll::Pending
731            }
732        }
733    }
734
735    impl Timeout {
736        fn new(duration: Duration) -> Self {
737            let shared_state = Arc::new(Mutex::new(TimeoutState {
738                completed: false,
739                waker: None,
740            }));
741
742            // Spawn the new thread
743            let thread_shared_state = shared_state.clone();
744            thread::spawn(move || {
745                thread::sleep(duration);
746                let mut shared_state = thread_shared_state.lock();
747                shared_state.completed = true;
748                if let Some(waker) = shared_state.waker.take() {
749                    waker.wake()
750                }
751            });
752
753            Timeout { shared_state }
754        }
755    }
756
757    /// Skips a test case if IO-Uring is not supported (this is necessary because IO-Uring is not yet supported
758    /// by the official WSL2 kernel).
759    macro_rules! skip_if_no_io_uring_support {
760        () => {
761            if IoRing::new(1).is_err() {
762                println!("Test case skipped (no IO-Uring support)");
763                return;
764            }
765        };
766    }
767
768    #[test]
769    fn test_task_executor() {
770        skip_if_no_io_uring_support!();
771        let tp = SingleThreadPool::new().unwrap();
772
773        let (tx, rx) = std::sync::mpsc::channel();
774
775        tp.initiator()
776            .spawn("test", async move {
777                let now = Instant::now();
778                Timeout::new(Duration::from_secs(2)).await;
779                assert!(now.elapsed().as_secs() >= 2);
780                tx.send(()).unwrap();
781            })
782            .detach();
783
784        rx.recv().unwrap();
785    }
786
787    #[test]
788    fn test_local_task_executor() {
789        skip_if_no_io_uring_support!();
790        let tp = SingleThreadPool::new().unwrap();
791
792        let (tx, rx) = std::sync::mpsc::channel();
793
794        tp.initiator()
795            .spawn("test", async move {
796                let now = Instant::now();
797                Timeout::new(Duration::from_secs(2)).await;
798                assert!(now.elapsed().as_secs() >= 2);
799                tx.send(()).unwrap();
800            })
801            .detach();
802
803        rx.recv().unwrap();
804    }
805
806    #[test]
807    fn test_serial_io() {
808        skip_if_no_io_uring_support!();
809        let (_env, test) = new_test();
810
811        block_on(async move {
812            let _ = &test;
813            let mut write_buf = vec![0u8; PAGE_SIZE];
814            for (i, b) in write_buf.iter_mut().enumerate() {
815                *b = i as u8;
816            }
817
818            let sqe = opcode::Write::new(
819                types::Fd(test.file.as_fd().as_raw_fd()),
820                write_buf.as_ptr(),
821                write_buf.len() as _,
822            )
823            .offset(0)
824            .build();
825
826            // SAFETY: The only memory being referenced in the submission is write_buf.
827            let mut write_io = unsafe { Io::new(test.tp.initiator(), sqe, write_buf) };
828            (&mut write_io).await.unwrap();
829            let write_buf = write_io.into_mem();
830
831            let sqe = opcode::Fsync::new(types::Fd(test.file.as_fd().as_raw_fd())).build();
832            // SAFETY: the Fsync entry does not reference any external memory.
833            unsafe {
834                Io::new(test.tp.initiator(), sqe, ()).await.unwrap();
835            }
836
837            let mut read_buf = vec![0u8; PAGE_SIZE];
838            let sqe = opcode::Read::new(
839                types::Fd(test.file.as_fd().as_raw_fd()),
840                read_buf.as_mut_ptr(),
841                read_buf.len() as _,
842            )
843            .offset(0)
844            .build();
845
846            // SAFETY: The only memory being referenced in the submission is read_buf.
847            let mut read_io = unsafe { Io::new(test.tp.initiator(), sqe, read_buf) };
848            (&mut read_io).await.unwrap();
849            let read_buf = read_io.into_mem();
850
851            assert_eq!(&write_buf[..], &read_buf[..]);
852        });
853    }
854
855    #[test]
856    fn test_stack_io() {
857        skip_if_no_io_uring_support!();
858        let (_env, test) = new_test();
859
860        block_on(async move {
861            let _ = &test;
862            let mut write_buf = [0; 100];
863            for (i, b) in write_buf.iter_mut().enumerate() {
864                *b = i as u8;
865            }
866
867            // SAFETY: The only memory being referenced in the submission is write_buf.
868            let (r, write_buf) = unsafe {
869                test.tp
870                    .initiator()
871                    .issue_io(write_buf, |write_buf| {
872                        opcode::Write::new(
873                            types::Fd(test.file.as_fd().as_raw_fd()),
874                            write_buf.as_ptr(),
875                            write_buf.len() as _,
876                        )
877                        .offset(0)
878                        .build()
879                    })
880                    .await
881            };
882            r.unwrap();
883
884            // SAFETY: the Fsync entry does not reference any external memory.
885            unsafe {
886                test.tp
887                    .initiator()
888                    .issue_io((), |_| {
889                        opcode::Fsync::new(types::Fd(test.file.as_fd().as_raw_fd())).build()
890                    })
891                    .await
892                    .0
893                    .unwrap();
894            }
895
896            let read_buf = [0u8; 100];
897            // SAFETY: the buffer is owned by the IO for its lifetime.
898            let (r, read_buf) = unsafe {
899                test.tp
900                    .initiator()
901                    .issue_io(read_buf, |read_buf| {
902                        opcode::Read::new(
903                            types::Fd(test.file.as_fd().as_raw_fd()),
904                            read_buf.as_mut_ptr(),
905                            read_buf.len() as _,
906                        )
907                        .offset(0)
908                        .build()
909                    })
910                    .await
911            };
912            r.unwrap();
913
914            assert_eq!(&write_buf[..], &read_buf[..]);
915        });
916    }
917
918    // TODO: This test requires higher memlock limits that scale with processor count, as set in
919    //       /etc/security/limits.conf and with ulimit -l.
920    //
921    //       Disable these in CI for now until the code is more aware of limits and can handle them and/or io-uring no
922    //       longer requires locked pages. A 16 core build agent requires more than the default set.
923    #[test]
924    #[cfg(not(feature = "ci"))]
925    fn test_split_io() {
926        skip_if_no_io_uring_support!();
927        let (_env, test) = new_test();
928
929        block_on(async move {
930            let _ = &test;
931            let mut write_buf1 = vec![0u8; PAGE_SIZE];
932            for (i, b) in write_buf1.iter_mut().enumerate() {
933                *b = i as u8;
934            }
935            let sqe1 = opcode::Write::new(
936                types::Fd(test.file.as_fd().as_raw_fd()),
937                write_buf1.as_mut_ptr(),
938                write_buf1.len() as _,
939            )
940            .offset(0)
941            .build();
942            // SAFETY: The only memory being referenced in the submission is write_buf1.
943            let write1 = unsafe { Io::new(test.tp.initiator(), sqe1, write_buf1) };
944
945            let mut write_buf2 = vec![0u8; PAGE_SIZE];
946            for (i, b) in write_buf2.iter_mut().enumerate() {
947                *b = i as u8;
948            }
949            let sqe2 = opcode::Write::new(
950                types::Fd(test.file.as_fd().as_raw_fd()),
951                write_buf2.as_mut_ptr(),
952                write_buf2.len() as _,
953            )
954            .offset(4096)
955            .build();
956            // SAFETY: The only memory being referenced in the submission is write_buf2.
957            let write2 = unsafe { Io::new(test.tp.initiator(), sqe2, write_buf2) };
958
959            let (r1, r2) = futures::join!(write1, write2);
960            r1.unwrap();
961            r2.unwrap();
962        });
963    }
964
965    // TODO: This test requires higher memlock limits that scale with processor count, as set in
966    //       /etc/security/limits.conf and with ulimit -l.
967    //
968    //       Disable these in CI for now until the code is more aware of limits and can handle them and/or io-uring no
969    //       longer requires locked pages. A 16 core build agent requires more than the default set.
970    #[test]
971    #[cfg(not(feature = "ci"))]
972    fn test_tp_io() {
973        skip_if_no_io_uring_support!();
974        let (_env, test) = new_test();
975
976        test.tp
977            .initiator()
978            .clone()
979            .spawn("test", async move {
980                let _ = &test;
981                let mut write_buf = vec![0u8; PAGE_SIZE];
982                for (i, b) in write_buf.iter_mut().enumerate() {
983                    *b = i as u8;
984                }
985
986                let sqe = opcode::Write::new(
987                    types::Fd(test.file.as_fd().as_raw_fd()),
988                    write_buf.as_mut_ptr(),
989                    write_buf.len() as _,
990                )
991                .offset(0)
992                .build();
993
994                // SAFETY: The only memory being referenced in the submission is write_buf.
995                let mut write_io = unsafe { Io::new(test.tp.initiator(), sqe, write_buf) };
996                (&mut write_io).await.unwrap();
997                let write_buf = write_io.into_mem();
998
999                let mut read_buf = vec![0u8; PAGE_SIZE];
1000
1001                let sqe = opcode::Read::new(
1002                    types::Fd(test.file.as_fd().as_raw_fd()),
1003                    read_buf.as_mut_ptr(),
1004                    read_buf.len() as _,
1005                )
1006                .offset(0)
1007                .build();
1008
1009                // SAFETY: The only memory being referenced in the submission is read_buf.
1010                let mut read_io = unsafe { Io::new(test.tp.initiator(), sqe, read_buf) };
1011                (&mut read_io).await.unwrap();
1012                let read_buf = read_io.into_mem();
1013
1014                assert_eq!(&write_buf[..], &read_buf[..]);
1015            })
1016            .detach();
1017    }
1018
1019    #[test]
1020    fn test_run_until_none() {
1021        skip_if_no_io_uring_support!();
1022        let (send, recv) = futures::channel::oneshot::channel();
1023        let (send2, recv2) = futures::channel::oneshot::channel();
1024        let pool = IoUringPool::new("test", 16).unwrap();
1025        let done = Arc::new(AtomicBool::new(false));
1026        pool.client()
1027            .initiator()
1028            .spawn("hmm", {
1029                async move {
1030                    recv.await.unwrap();
1031                    send2.send(()).unwrap();
1032                }
1033            })
1034            .detach();
1035        pool.client().set_idle_task({
1036            let done = done.clone();
1037            |_ctl| async move {
1038                send.send(()).unwrap();
1039                recv2.await.unwrap();
1040                done.store(true, Ordering::SeqCst);
1041            }
1042        });
1043        pool.run();
1044        assert!(done.load(Ordering::SeqCst));
1045    }
1046}