Skip to main content

compio_runtime/
lib.rs

1//! The compio runtime.
2//!
3//! ```
4//! let ans = compio_runtime::Runtime::new().unwrap().block_on(async {
5//!     println!("Hello world!");
6//!     42
7//! });
8//! assert_eq!(ans, 42);
9//! ```
10
11#![cfg_attr(docsrs, feature(doc_cfg))]
12#![cfg_attr(feature = "current_thread_id", feature(current_thread_id))]
13#![cfg_attr(feature = "read_buf", feature(read_buf, core_io_borrowed_buf))]
14#![allow(unused_features)]
15#![warn(missing_docs)]
16#![deny(rustdoc::broken_intra_doc_links)]
17#![doc(
18    html_logo_url = "https://github.com/compio-rs/compio-logo/raw/refs/heads/master/generated/colored-bold.svg"
19)]
20#![doc(
21    html_favicon_url = "https://github.com/compio-rs/compio-logo/raw/refs/heads/master/generated/colored-bold.svg"
22)]
23
24mod affinity;
25mod attacher;
26mod cancel;
27mod future;
28mod waker;
29
30pub mod fd;
31
32#[cfg(feature = "time")]
33pub mod time;
34
35use std::{
36    cell::RefCell,
37    collections::HashSet,
38    fmt::Debug,
39    future::Future,
40    io,
41    panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
42    rc::Rc,
43    task::{Context, Poll, Waker},
44    time::Duration,
45};
46
47use compio_buf::{BufResult, IntoInner};
48use compio_driver::{AsRawFd, DriverType, OpCode, Proactor, ProactorBuilder, RawFd, op::Asyncify};
49pub use compio_driver::{BufferPool, ErrorExt};
50use compio_executor::{Executor, ExecutorConfig};
51pub use compio_executor::{JoinError, JoinHandle, ResumeUnwind, SpawnMeta, console};
52use compio_log::{debug, instrument};
53
54use crate::affinity::bind_to_cpu_set;
55#[cfg(feature = "time")]
56use crate::time::TimerRuntime;
57pub use crate::{attacher::*, cancel::CancelToken, future::*};
58
59scoped_tls::scoped_thread_local!(static CURRENT_RUNTIME: Runtime);
60
61#[cold]
62fn not_in_compio_runtime() -> ! {
63    panic!("not in a compio runtime")
64}
65
66/// The async runtime of compio.
67///
68/// It is a thread-local runtime, meaning it cannot be sent to other threads.
69#[derive(Clone)]
70pub struct Runtime {
71    executor: Rc<Executor>,
72    driver: Rc<RefCell<Proactor>>,
73    #[cfg(feature = "time")]
74    timer_runtime: Rc<RefCell<TimerRuntime>>,
75}
76
77impl Debug for Runtime {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        let mut s = f.debug_struct("Runtime");
80        s.field("executor", &self.executor);
81        s.field("driver", &"...");
82        #[cfg(feature = "time")]
83        s.field("timer_runtime", &"...");
84        s.finish()
85    }
86}
87
88impl Runtime {
89    /// Create [`Runtime`] with default config.
90    pub fn new() -> io::Result<Self> {
91        Self::builder().build()
92    }
93
94    /// Create a builder for [`Runtime`].
95    pub fn builder() -> RuntimeBuilder {
96        RuntimeBuilder::new()
97    }
98
99    /// The current driver type.
100    pub fn driver_type(&self) -> DriverType {
101        self.driver.borrow().driver_type()
102    }
103
104    /// Try to perform a function on the current runtime, and if no runtime is
105    /// running, return the function back.
106    pub fn try_with_current<T, F: FnOnce(&Self) -> T>(f: F) -> Result<T, F> {
107        if CURRENT_RUNTIME.is_set() {
108            Ok(CURRENT_RUNTIME.with(f))
109        } else {
110            Err(f)
111        }
112    }
113
114    /// Perform a function on the current runtime.
115    ///
116    /// ## Panics
117    ///
118    /// This method will panic if there is no running [`Runtime`].
119    pub fn with_current<T, F: FnOnce(&Self) -> T>(f: F) -> T {
120        if CURRENT_RUNTIME.is_set() {
121            CURRENT_RUNTIME.with(f)
122        } else {
123            not_in_compio_runtime()
124        }
125    }
126
127    /// Try to get the current runtime, and if no runtime is running, return
128    /// `None`.
129    pub fn try_current() -> Option<Self> {
130        if CURRENT_RUNTIME.is_set() {
131            Some(CURRENT_RUNTIME.with(|r| r.clone()))
132        } else {
133            None
134        }
135    }
136
137    /// Get the current runtime.
138    ///
139    /// # Panics
140    ///
141    /// This method will panic if there is no running [`Runtime`].
142    pub fn current() -> Self {
143        if CURRENT_RUNTIME.is_set() {
144            CURRENT_RUNTIME.with(|r| r.clone())
145        } else {
146            not_in_compio_runtime()
147        }
148    }
149
150    /// Set this runtime as current runtime, and perform a function in the
151    /// current scope.
152    pub fn enter<T, F: FnOnce() -> T>(&self, f: F) -> T {
153        CURRENT_RUNTIME.set(self, f)
154    }
155
156    /// Low level API to control the runtime.
157    ///
158    /// Run the scheduled tasks.
159    ///
160    /// The return value indicates whether there are still tasks in the queue.
161    pub fn run(&self) -> bool {
162        self.executor.tick()
163    }
164
165    /// Low level API to control the runtime.
166    ///
167    /// Create a waker that always notifies the runtime when woken.
168    pub fn waker(&self) -> Waker {
169        self.driver.borrow().waker()
170    }
171
172    /// Block on the future till it completes.
173    #[track_caller]
174    pub fn block_on<F: Future>(&self, future: F) -> F::Output {
175        self.block_on_at(future, SpawnMeta::capture())
176    }
177
178    /// Block on the future till it completes, attributing the task it shows up
179    /// as in the console to the given [`SpawnMeta`].
180    ///
181    /// This is for the runtimes compio blocks on itself, whose location points
182    /// into compio rather than into the code of whoever set them running.
183    pub fn block_on_at<F: Future>(&self, future: F, meta: SpawnMeta) -> F::Output {
184        let future = console::instrument_block_on(meta, future);
185        let result = catch_unwind(AssertUnwindSafe(|| {
186            self.enter(|| {
187                let waker = self.waker();
188                let mut context = Context::from_waker(&waker);
189                let mut future = std::pin::pin!(future);
190                loop {
191                    if let Poll::Ready(result) = future.as_mut().poll(&mut context) {
192                        self.run();
193                        return result;
194                    }
195                    let remaining_tasks = self.run();
196                    if remaining_tasks {
197                        self.poll_with(Some(Duration::ZERO));
198                    } else {
199                        self.poll();
200                    }
201                }
202            })
203        }));
204
205        match result {
206            Ok(output) => output,
207            Err(payload) => {
208                // Pending tasks must be cleared after the active unwind has
209                // ended so their futures can be dropped safely.
210                self.enter(|| self.executor.clear());
211                resume_unwind(payload)
212            }
213        }
214    }
215
216    /// Spawns a new asynchronous task, returning a [`JoinHandle`] for it.
217    ///
218    /// Spawning a task enables the task to execute concurrently to other tasks.
219    /// There is no guarantee that a spawned task will execute to completion.
220    #[track_caller]
221    pub fn spawn<F: Future + 'static>(&self, future: F) -> JoinHandle<F::Output> {
222        self.spawn_at(future, SpawnMeta::capture())
223    }
224
225    /// Spawns a new asynchronous task, attributing it to `meta` instead of to
226    /// the caller.
227    ///
228    /// This is what wrappers around [`spawn`] want, so that [`tokio-console`]
229    /// blames their own caller instead of themselves. [`SpawnMeta`] is only
230    /// interesting to it, and is a zero-sized no-op without the `console`
231    /// feature.
232    ///
233    /// [`spawn`]: Self::spawn
234    /// [`tokio-console`]: crate::console
235    pub fn spawn_at<F: Future + 'static>(
236        &self,
237        future: F,
238        meta: SpawnMeta,
239    ) -> JoinHandle<F::Output> {
240        self.executor.spawn_at(future, meta)
241    }
242
243    /// Spawns a blocking task in a new thread, and wait for it.
244    ///
245    /// The task will not be cancelled even if the future is dropped.
246    #[track_caller]
247    pub fn spawn_blocking<T: Send + 'static>(
248        &self,
249        f: impl (FnOnce() -> T) + Send + 'static,
250    ) -> JoinHandle<T> {
251        self.spawn_blocking_at(f, SpawnMeta::capture())
252    }
253
254    /// Spawns a blocking task in a new thread, attributing it to `meta` instead
255    /// of to the caller.
256    ///
257    /// See [`spawn_at`] for what `meta` is good for.
258    ///
259    /// [`spawn_at`]: Self::spawn_at
260    pub fn spawn_blocking_at<T: Send + 'static>(
261        &self,
262        f: impl (FnOnce() -> T) + Send + 'static,
263        meta: SpawnMeta,
264    ) -> JoinHandle<T> {
265        use futures_util::FutureExt;
266
267        // The closure is what the console reports as the blocking task, so the
268        // future waiting for it below is not reported at all.
269        let f = console::instrument_blocking(meta, f);
270        let op = Asyncify::new(move || {
271            // TODO: Refactor blocking pool and handle panic within worker and propagate it
272            // back
273            let res = f();
274            BufResult(Ok(0), res)
275        });
276        let submit = self.submit(op);
277        self.spawn_at(submit.map(|res| res.1.into_inner()), SpawnMeta::untracked())
278    }
279
280    /// Attach a raw file descriptor/handle/socket to the runtime.
281    ///
282    /// You only need this when authoring your own high-level APIs. High-level
283    /// resources in this crate are attached automatically.
284    pub fn attach(&self, fd: RawFd) -> io::Result<()> {
285        self.driver.borrow_mut().attach(fd)
286    }
287
288    /// Submit an operation to the runtime.
289    ///
290    /// You only need this when authoring your own [`OpCode`].
291    pub fn submit<T: OpCode + 'static>(&self, op: T) -> Submit<T> {
292        Submit::new(self.driver.clone(), op)
293    }
294
295    /// Submit a multishot operation to the runtime.
296    ///
297    /// You only need this when authoring your own [`OpCode`].
298    pub fn submit_multi<T: OpCode + 'static>(&self, op: T) -> SubmitMulti<T> {
299        SubmitMulti::new(self.driver.clone(), op)
300    }
301
302    /// Flush the driver and return whether the driver has been notified.
303    ///
304    /// See [`Proactor::flush`] for more details.
305    pub fn flush(&self) -> bool {
306        self.driver.borrow_mut().flush()
307    }
308
309    /// Low level API to control the runtime.
310    ///
311    /// Get the timeout value to be passed to [`Proactor::poll`].
312    pub fn current_timeout(&self) -> Option<Duration> {
313        #[cfg(not(feature = "time"))]
314        let timeout = None;
315        #[cfg(feature = "time")]
316        let timeout = self.timer_runtime.borrow().min_timeout();
317        timeout
318    }
319
320    /// Low level API to control the runtime.
321    ///
322    /// Poll the inner proactor. It is equal to calling [`Runtime::poll_with`]
323    /// with [`Runtime::current_timeout`].
324    pub fn poll(&self) {
325        instrument!(compio_log::Level::DEBUG, "poll");
326        let timeout = self.current_timeout();
327        debug!("timeout: {:?}", timeout);
328        self.poll_with(timeout)
329    }
330
331    /// Low level API to control the runtime.
332    ///
333    /// Poll the inner proactor with a custom timeout.
334    pub fn poll_with(&self, timeout: Option<Duration>) {
335        instrument!(compio_log::Level::DEBUG, "poll_with");
336
337        let mut driver = self.driver.borrow_mut();
338        match driver.poll(timeout) {
339            Ok(()) => {}
340            Err(e) => match e.kind() {
341                io::ErrorKind::TimedOut | io::ErrorKind::Interrupted => {
342                    debug!("expected error: {e}");
343                }
344                _ => panic!("{e:?}"),
345            },
346        }
347        #[cfg(feature = "time")]
348        self.timer_runtime.borrow_mut().wake();
349    }
350
351    /// Get buffer pool of the runtime.
352    ///
353    /// This will lazily initialize the pool at the first time it's accessed,
354    /// and future access to the pool will be cheap and infallible.
355    pub fn buffer_pool(&self) -> io::Result<BufferPool> {
356        self.driver.borrow_mut().buffer_pool()
357    }
358
359    /// Register file descriptors for fixed-file operations.
360    ///
361    /// This is only supported on io-uring driver, and will return an
362    /// [`Unsupported`] io error on all other drivers.
363    ///
364    /// [`Unsupported`]: std::io::ErrorKind::Unsupported
365    pub fn register_files(&self, fds: &[RawFd]) -> io::Result<()> {
366        self.driver.borrow_mut().register_files(fds)
367    }
368
369    /// Unregister previously registered file descriptors.
370    ///
371    /// This is only supported on io-uring driver, and will return an
372    /// [`Unsupported`] io error on all other drivers.
373    ///
374    /// [`Unsupported`]: std::io::ErrorKind::Unsupported
375    pub fn unregister_files(&self) -> io::Result<()> {
376        self.driver.borrow_mut().unregister_files()
377    }
378
379    /// Register the personality for the runtime.
380    ///
381    /// This is only supported on io-uring driver, and will return an
382    /// [`Unsupported`] io error on all other drivers.
383    ///
384    /// The returned personality can be used with
385    /// [`FutureExt::with_personality`].
386    ///
387    /// [`Unsupported`]: std::io::ErrorKind::Unsupported
388    pub fn register_personality(&self) -> io::Result<u16> {
389        self.driver.borrow_mut().register_personality()
390    }
391
392    /// Unregister the given personality for the runtime.
393    ///
394    /// This is only supported on io-uring driver, and will return an
395    /// [`Unsupported`] io error on all other drivers.
396    ///
397    /// [`Unsupported`]: std::io::ErrorKind::Unsupported
398    pub fn unregister_personality(&self, personality: u16) -> io::Result<()> {
399        self.driver.borrow_mut().unregister_personality(personality)
400    }
401}
402
403impl Drop for Runtime {
404    fn drop(&mut self) {
405        // this is not the last runtime reference, no need to clear
406        if Rc::strong_count(&self.executor) > 1 {
407            return;
408        }
409
410        self.enter(|| {
411            self.executor.clear();
412        })
413    }
414}
415
416impl AsRawFd for Runtime {
417    fn as_raw_fd(&self) -> RawFd {
418        self.driver.borrow().as_raw_fd()
419    }
420}
421
422#[cfg(feature = "criterion")]
423impl criterion::async_executor::AsyncExecutor for Runtime {
424    fn block_on<T>(&self, future: impl Future<Output = T>) -> T {
425        self.block_on(future)
426    }
427}
428
429#[cfg(feature = "criterion")]
430impl criterion::async_executor::AsyncExecutor for &Runtime {
431    fn block_on<T>(&self, future: impl Future<Output = T>) -> T {
432        (**self).block_on(future)
433    }
434}
435
436/// Builder for [`Runtime`].
437#[derive(Debug, Clone)]
438pub struct RuntimeBuilder {
439    proactor_builder: ProactorBuilder,
440    thread_affinity: HashSet<usize>,
441    sync_queue_size: usize,
442    local_queue_size: usize,
443    event_interval: u32,
444}
445
446impl Default for RuntimeBuilder {
447    fn default() -> Self {
448        Self::new()
449    }
450}
451
452impl RuntimeBuilder {
453    /// Create the builder with default config.
454    pub fn new() -> Self {
455        Self {
456            proactor_builder: ProactorBuilder::new(),
457            event_interval: 61,
458            sync_queue_size: 64,
459            local_queue_size: 64,
460            thread_affinity: HashSet::new(),
461        }
462    }
463
464    /// Replace proactor builder.
465    pub fn with_proactor(&mut self, builder: ProactorBuilder) -> &mut Self {
466        self.proactor_builder = builder;
467        self
468    }
469
470    /// Sets the thread affinity for the runtime.
471    pub fn thread_affinity(&mut self, cpus: HashSet<usize>) -> &mut Self {
472        self.thread_affinity = cpus;
473        self
474    }
475
476    /// Sets the number of scheduler ticks after which the scheduler will poll
477    /// for external events (timers, I/O, and so on).
478    ///
479    /// A scheduler “tick” roughly corresponds to one poll invocation on a task.
480    pub fn event_interval(&mut self, val: usize) -> &mut Self {
481        self.event_interval = val as _;
482        self
483    }
484
485    /// The size of the sync queue, which is used to wake up tasks from other
486    /// threads (remote).
487    ///
488    /// This is fixed and will create backpressure in other remote threads when
489    /// full.
490    pub fn sync_queue_size(&mut self, val: usize) -> &mut Self {
491        self.sync_queue_size = val;
492        self
493    }
494
495    /// The size of the local queues, which is used to wake up tasks within the
496    /// same thread.
497    ///
498    /// This is dynamically resized to avoid blocking.
499    pub fn local_queue_size(&mut self, val: usize) -> &mut Self {
500        self.local_queue_size = val;
501        self
502    }
503
504    /// Build [`Runtime`].
505    pub fn build(&self) -> io::Result<Runtime> {
506        let RuntimeBuilder {
507            proactor_builder,
508            thread_affinity,
509            sync_queue_size,
510            local_queue_size,
511            event_interval,
512        } = self;
513
514        if !thread_affinity.is_empty() {
515            bind_to_cpu_set(thread_affinity);
516        }
517        let driver = proactor_builder.build()?;
518        let executor = Executor::with_config(ExecutorConfig {
519            max_interval: *event_interval,
520            sync_queue_size: *sync_queue_size,
521            local_queue_size: *local_queue_size,
522            waker: Some(driver.waker()),
523        });
524        Ok(Runtime {
525            executor: Rc::new(executor),
526            driver: Rc::new(RefCell::new(driver)),
527            #[cfg(feature = "time")]
528            timer_runtime: Rc::new(RefCell::new(TimerRuntime::new())),
529        })
530    }
531}
532
533/// Spawns a new asynchronous task, returning a [`JoinHandle`] for it.
534///
535/// Spawning a task enables the task to execute concurrently to other tasks.
536/// There is no guarantee that a spawned task will execute to completion.
537///
538/// ```
539/// # compio_runtime::Runtime::new().unwrap().block_on(async {
540/// use compio_runtime::ResumeUnwind;
541///
542/// let task = compio_runtime::spawn(async {
543///     println!("Hello from a spawned task!");
544///     42
545/// });
546///
547/// assert_eq!(
548///     task.await.resume_unwind().expect("shouldn't be cancelled"),
549///     42
550/// );
551/// # })
552/// ```
553///
554/// ## Panics
555///
556/// This method doesn't create runtime. It tries to obtain the current runtime
557/// by [`Runtime::with_current`].
558#[track_caller]
559pub fn spawn<F: Future + 'static>(future: F) -> JoinHandle<F::Output> {
560    // Capture outside of the closure: `#[track_caller]` does not reach into it,
561    // so inlining this would attribute every task to the line above.
562    let meta = SpawnMeta::capture();
563    spawn_at(future, meta)
564}
565
566/// Spawns a new asynchronous task, attributing it to `meta` instead of to the
567/// caller.
568///
569/// See [`Runtime::spawn_at`] for what `meta` is good for.
570///
571/// ## Panics
572///
573/// This method doesn't create runtime. It tries to obtain the current runtime
574/// by [`Runtime::with_current`].
575pub fn spawn_at<F: Future + 'static>(future: F, meta: SpawnMeta) -> JoinHandle<F::Output> {
576    Runtime::with_current(|r| r.spawn_at(future, meta))
577}
578
579/// Spawns a blocking task in a new thread, and wait for it.
580///
581/// The task will not be cancelled even if the future is dropped.
582///
583/// ## Panics
584///
585/// This method doesn't create runtime. It tries to obtain the current runtime
586/// by [`Runtime::with_current`].
587#[track_caller]
588pub fn spawn_blocking<T: Send + 'static>(
589    f: impl (FnOnce() -> T) + Send + 'static,
590) -> JoinHandle<T> {
591    // See the note in `spawn` on why this is not inlined below.
592    let meta = SpawnMeta::capture();
593    spawn_blocking_at(f, meta)
594}
595
596/// Spawns a blocking task in a new thread, attributing it to `meta` instead of
597/// to the caller.
598///
599/// See [`Runtime::spawn_at`] for what `meta` is good for.
600///
601/// ## Panics
602///
603/// This method doesn't create runtime. It tries to obtain the current runtime
604/// by [`Runtime::with_current`].
605pub fn spawn_blocking_at<T: Send + 'static>(
606    f: impl (FnOnce() -> T) + Send + 'static,
607    meta: SpawnMeta,
608) -> JoinHandle<T> {
609    Runtime::with_current(|r| r.spawn_blocking_at(f, meta))
610}
611
612/// Submit an operation to the current runtime, and return a future for it.
613///
614/// ## Panics
615///
616/// This method doesn't create runtime and will panic if it's not within a
617/// runtime. It tries to obtain the current runtime with
618/// [`Runtime::with_current`].
619pub fn submit<T: OpCode + 'static>(op: T) -> Submit<T> {
620    Runtime::with_current(|r| r.submit(op))
621}
622
623/// Submit a multishot operation to the current runtime, and return a stream for
624/// it.
625///
626/// ## Panics
627///
628/// This method doesn't create runtime and will panic if it's not within a
629/// runtime. It tries to obtain the current runtime with
630/// [`Runtime::with_current`].
631pub fn submit_multi<T: OpCode + 'static>(op: T) -> SubmitMulti<T> {
632    Runtime::with_current(|r| r.submit_multi(op))
633}
634
635/// Register file descriptors for fixed-file operations with the current
636/// runtime's io_uring instance.
637///
638/// This only works on `io_uring` driver. It will return an [`Unsupported`]
639/// error on other drivers.
640///
641/// ## Panics
642///
643/// This method doesn't create runtime. It tries to obtain the current runtime
644/// by [`Runtime::with_current`].
645///
646/// [`Unsupported`]: std::io::ErrorKind::Unsupported
647pub fn register_files(fds: &[RawFd]) -> io::Result<()> {
648    Runtime::with_current(|r| r.register_files(fds))
649}
650
651/// Unregister previously registered file descriptors from the current
652/// runtime's io_uring instance.
653///
654/// This only works on `io_uring` driver. It will return an [`Unsupported`]
655/// error on other drivers.
656///
657/// ## Panics
658///
659/// This method doesn't create runtime. It tries to obtain the current runtime
660/// by [`Runtime::with_current`].
661///
662/// [`Unsupported`]: std::io::ErrorKind::Unsupported
663pub fn unregister_files() -> io::Result<()> {
664    Runtime::with_current(|r| r.unregister_files())
665}