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    /// Wrappers around [`spawn`] can use `meta` to track the original
229    /// caller, so that [`tokio-console`] blames that caller instead of the
230    /// wrapper. [`SpawnMeta`] only matters to the console, and is a
231    /// zero-sized no-op without the `console` 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    /// Returns the number of alive tasks in the runtime.
244    ///
245    /// This counts the tasks created by [`spawn`] and [`spawn_blocking`] that
246    /// have not finished yet, whether they are scheduled to run or waiting to
247    /// be woken. The future passed to [`block_on`] is not a task and is not
248    /// counted.
249    ///
250    /// A finished or cancelled task stops being counted once the runtime has
251    /// run it for the last time, which may be later than the moment its
252    /// [`JoinHandle`] reports it.
253    ///
254    /// [`spawn`]: Self::spawn
255    /// [`spawn_blocking`]: Self::spawn_blocking
256    /// [`block_on`]: Self::block_on
257    pub fn num_alive_tasks(&self) -> usize {
258        self.executor.num_alive_tasks()
259    }
260
261    /// Spawns a blocking task in the driver's thread pool, returning a handle
262    /// for it.
263    ///
264    /// This thread pool is limited, and is also used by the driver for
265    /// operations that don't have native asynchronous support. See
266    /// [`ProactorBuilder::thread_pool_limit`]. If all the threads are busy
267    /// when the task is submitted, the runtime thread blocks until one is
268    /// available.
269    ///
270    /// The task can be cancelled by dropping the returned [`JoinHandle`], but
271    /// the execution won't be interrupted if already started. In that case,
272    /// it runs to completion and the result is discarded.
273    #[track_caller]
274    pub fn spawn_blocking<T: Send + 'static>(
275        &self,
276        f: impl (FnOnce() -> T) + Send + 'static,
277    ) -> JoinHandle<T> {
278        self.spawn_blocking_at(f, SpawnMeta::capture())
279    }
280
281    /// Spawns a blocking task in the driver's thread pool, attributing it to
282    /// `meta` instead of to the caller. See [`spawn_blocking`] for details.
283    ///
284    /// See [`spawn_at`] for what `meta` is good for.
285    ///
286    /// [`spawn_at`]: Self::spawn_at
287    pub fn spawn_blocking_at<T: Send + 'static>(
288        &self,
289        f: impl (FnOnce() -> T) + Send + 'static,
290        meta: SpawnMeta,
291    ) -> JoinHandle<T> {
292        use futures_util::FutureExt;
293
294        // The closure is what the console reports as the blocking task, so the
295        // future waiting for it below is not reported at all.
296        let f = console::instrument_blocking(meta, f);
297        let op = Asyncify::new(move || {
298            // TODO: Refactor blocking pool and handle panic within worker and
299            // propagate it back
300            let res = f();
301            BufResult(Ok(0), res)
302        });
303        let submit = self.submit(op);
304        self.spawn_at(submit.map(|res| res.1.into_inner()), SpawnMeta::untracked())
305    }
306
307    /// Attach a raw file descriptor/handle/socket to the runtime.
308    ///
309    /// You only need this when authoring your own high-level APIs. High-level
310    /// resources in this crate are attached automatically.
311    pub fn attach(&self, fd: RawFd) -> io::Result<()> {
312        self.driver.borrow_mut().attach(fd)
313    }
314
315    /// Submit an operation to the runtime.
316    ///
317    /// You only need this when authoring your own [`OpCode`].
318    pub fn submit<T: OpCode + 'static>(&self, op: T) -> Submit<T> {
319        Submit::new(self.driver.clone(), op)
320    }
321
322    /// Submit a multishot operation to the runtime.
323    ///
324    /// You only need this when authoring your own [`OpCode`].
325    pub fn submit_multi<T: OpCode + 'static>(&self, op: T) -> SubmitMulti<T> {
326        SubmitMulti::new(self.driver.clone(), op)
327    }
328
329    /// Flush the driver and return whether the driver has been notified.
330    ///
331    /// See [`Proactor::flush`] for more details.
332    pub fn flush(&self) -> bool {
333        self.driver.borrow_mut().flush()
334    }
335
336    /// Low level API to control the runtime.
337    ///
338    /// Get the timeout value to be passed to [`Proactor::poll`].
339    pub fn current_timeout(&self) -> Option<Duration> {
340        #[cfg(not(feature = "time"))]
341        let timeout = None;
342        #[cfg(feature = "time")]
343        let timeout = self.timer_runtime.borrow().min_timeout();
344        timeout
345    }
346
347    /// Low level API to control the runtime.
348    ///
349    /// Poll the inner proactor. It is equal to calling [`Runtime::poll_with`]
350    /// with [`Runtime::current_timeout`].
351    pub fn poll(&self) {
352        instrument!(compio_log::Level::DEBUG, "poll");
353        let timeout = self.current_timeout();
354        debug!("timeout: {:?}", timeout);
355        self.poll_with(timeout)
356    }
357
358    /// Low level API to control the runtime.
359    ///
360    /// Poll the inner proactor with a custom timeout.
361    pub fn poll_with(&self, timeout: Option<Duration>) {
362        instrument!(compio_log::Level::DEBUG, "poll_with");
363
364        let mut driver = self.driver.borrow_mut();
365        match driver.poll(timeout) {
366            Ok(()) => {}
367            Err(e) => match e.kind() {
368                io::ErrorKind::TimedOut | io::ErrorKind::Interrupted => {
369                    debug!("expected error: {e}");
370                }
371                _ => panic!("{e:?}"),
372            },
373        }
374        #[cfg(feature = "time")]
375        self.timer_runtime.borrow_mut().wake();
376    }
377
378    /// Get buffer pool of the runtime.
379    ///
380    /// This will lazily initialize the pool at the first time it's accessed,
381    /// and future access to the pool will be cheap and infallible.
382    pub fn buffer_pool(&self) -> io::Result<BufferPool> {
383        self.driver.borrow_mut().buffer_pool()
384    }
385
386    /// Register file descriptors for fixed-file operations.
387    ///
388    /// This is only supported on io-uring driver, and will return an
389    /// [`Unsupported`] io error on all other drivers.
390    ///
391    /// [`Unsupported`]: std::io::ErrorKind::Unsupported
392    pub fn register_files(&self, fds: &[RawFd]) -> io::Result<()> {
393        self.driver.borrow_mut().register_files(fds)
394    }
395
396    /// Unregister previously registered file descriptors.
397    ///
398    /// This is only supported on io-uring driver, and will return an
399    /// [`Unsupported`] io error on all other drivers.
400    ///
401    /// [`Unsupported`]: std::io::ErrorKind::Unsupported
402    pub fn unregister_files(&self) -> io::Result<()> {
403        self.driver.borrow_mut().unregister_files()
404    }
405
406    /// Register the personality for the runtime.
407    ///
408    /// This is only supported on io-uring driver, and will return an
409    /// [`Unsupported`] io error on all other drivers.
410    ///
411    /// The returned personality can be used with
412    /// [`FutureExt::with_personality`].
413    ///
414    /// [`Unsupported`]: std::io::ErrorKind::Unsupported
415    pub fn register_personality(&self) -> io::Result<u16> {
416        self.driver.borrow_mut().register_personality()
417    }
418
419    /// Unregister the given personality for the runtime.
420    ///
421    /// This is only supported on io-uring driver, and will return an
422    /// [`Unsupported`] io error on all other drivers.
423    ///
424    /// [`Unsupported`]: std::io::ErrorKind::Unsupported
425    pub fn unregister_personality(&self, personality: u16) -> io::Result<()> {
426        self.driver.borrow_mut().unregister_personality(personality)
427    }
428}
429
430impl Drop for Runtime {
431    fn drop(&mut self) {
432        // this is not the last runtime reference, no need to clear
433        if Rc::strong_count(&self.executor) > 1 {
434            return;
435        }
436
437        self.enter(|| {
438            self.executor.clear();
439        })
440    }
441}
442
443impl AsRawFd for Runtime {
444    fn as_raw_fd(&self) -> RawFd {
445        self.driver.borrow().as_raw_fd()
446    }
447}
448
449#[cfg(feature = "criterion")]
450impl criterion::async_executor::AsyncExecutor for Runtime {
451    fn block_on<T>(&self, future: impl Future<Output = T>) -> T {
452        self.block_on(future)
453    }
454}
455
456#[cfg(feature = "criterion")]
457impl criterion::async_executor::AsyncExecutor for &Runtime {
458    fn block_on<T>(&self, future: impl Future<Output = T>) -> T {
459        (**self).block_on(future)
460    }
461}
462
463/// Builder for [`Runtime`].
464#[derive(Debug, Clone)]
465pub struct RuntimeBuilder {
466    proactor_builder: ProactorBuilder,
467    thread_affinity: HashSet<usize>,
468    local_queue_size: usize,
469    event_interval: u32,
470}
471
472impl Default for RuntimeBuilder {
473    fn default() -> Self {
474        Self::new()
475    }
476}
477
478impl RuntimeBuilder {
479    /// Create the builder with default config.
480    pub fn new() -> Self {
481        Self {
482            proactor_builder: ProactorBuilder::new(),
483            event_interval: 61,
484            local_queue_size: 64,
485            thread_affinity: HashSet::new(),
486        }
487    }
488
489    /// Replace proactor builder.
490    pub fn with_proactor(&mut self, builder: ProactorBuilder) -> &mut Self {
491        self.proactor_builder = builder;
492        self
493    }
494
495    /// Sets the thread affinity for the runtime.
496    pub fn thread_affinity(&mut self, cpus: HashSet<usize>) -> &mut Self {
497        self.thread_affinity = cpus;
498        self
499    }
500
501    /// Sets the number of scheduler ticks after which the scheduler will poll
502    /// for external events (timers, I/O, and so on).
503    ///
504    /// A scheduler “tick” roughly corresponds to one poll invocation on a task.
505    pub fn event_interval(&mut self, val: usize) -> &mut Self {
506        self.event_interval = val as _;
507        self
508    }
509
510    /// The size of the local queues, which is used to wake up tasks within the
511    /// same thread.
512    ///
513    /// This is dynamically resized to avoid blocking.
514    pub fn local_queue_size(&mut self, val: usize) -> &mut Self {
515        self.local_queue_size = val;
516        self
517    }
518
519    /// Build [`Runtime`].
520    pub fn build(&self) -> io::Result<Runtime> {
521        let RuntimeBuilder {
522            proactor_builder,
523            thread_affinity,
524            local_queue_size,
525            event_interval,
526        } = self;
527
528        if !thread_affinity.is_empty() {
529            bind_to_cpu_set(thread_affinity);
530        }
531        let driver = proactor_builder.build()?;
532        let executor = Executor::with_config(ExecutorConfig {
533            max_interval: *event_interval,
534            local_queue_size: *local_queue_size,
535            waker: Some(driver.waker()),
536        });
537        Ok(Runtime {
538            executor: Rc::new(executor),
539            driver: Rc::new(RefCell::new(driver)),
540            #[cfg(feature = "time")]
541            timer_runtime: Rc::new(RefCell::new(TimerRuntime::new())),
542        })
543    }
544}
545
546/// Spawns a new asynchronous task, returning a [`JoinHandle`] for it.
547///
548/// Spawning a task enables the task to execute concurrently to other tasks.
549/// There is no guarantee that a spawned task will execute to completion.
550///
551/// ```
552/// # compio_runtime::Runtime::new().unwrap().block_on(async {
553/// use compio_runtime::ResumeUnwind;
554///
555/// let task = compio_runtime::spawn(async {
556///     println!("Hello from a spawned task!");
557///     42
558/// });
559///
560/// assert_eq!(
561///     task.await.resume_unwind().expect("shouldn't be cancelled"),
562///     42
563/// );
564/// # })
565/// ```
566///
567/// ## Panics
568///
569/// This method doesn't create runtime. It tries to obtain the current runtime
570/// by [`Runtime::with_current`].
571#[track_caller]
572pub fn spawn<F: Future + 'static>(future: F) -> JoinHandle<F::Output> {
573    // Capture outside of the closure: `#[track_caller]` does not reach into it,
574    // so inlining this would attribute every task to the line above.
575    let meta = SpawnMeta::capture();
576    spawn_at(future, meta)
577}
578
579/// Spawns a new asynchronous task, attributing it to `meta` instead of to the
580/// caller.
581///
582/// See [`Runtime::spawn_at`] for what `meta` is good for.
583///
584/// ## Panics
585///
586/// This method doesn't create runtime. It tries to obtain the current runtime
587/// by [`Runtime::with_current`].
588pub fn spawn_at<F: Future + 'static>(future: F, meta: SpawnMeta) -> JoinHandle<F::Output> {
589    Runtime::with_current(|r| r.spawn_at(future, meta))
590}
591
592/// Spawns a blocking task in a new thread, and wait for it.
593///
594/// The task will not be cancelled even if the future is dropped.
595///
596/// ## Panics
597///
598/// This method doesn't create runtime. It tries to obtain the current runtime
599/// by [`Runtime::with_current`].
600#[track_caller]
601pub fn spawn_blocking<T: Send + 'static>(
602    f: impl (FnOnce() -> T) + Send + 'static,
603) -> JoinHandle<T> {
604    // See the note in `spawn` on why this is not inlined below.
605    let meta = SpawnMeta::capture();
606    spawn_blocking_at(f, meta)
607}
608
609/// Spawns a blocking task in a new thread, attributing it to `meta` instead of
610/// to the caller.
611///
612/// See [`Runtime::spawn_at`] for what `meta` is good for.
613///
614/// ## Panics
615///
616/// This method doesn't create runtime. It tries to obtain the current runtime
617/// by [`Runtime::with_current`].
618pub fn spawn_blocking_at<T: Send + 'static>(
619    f: impl (FnOnce() -> T) + Send + 'static,
620    meta: SpawnMeta,
621) -> JoinHandle<T> {
622    Runtime::with_current(|r| r.spawn_blocking_at(f, meta))
623}
624
625/// Submit an operation to the current runtime, and return a future for it.
626///
627/// ## Panics
628///
629/// This method doesn't create runtime and will panic if it's not within a
630/// runtime. It tries to obtain the current runtime with
631/// [`Runtime::with_current`].
632pub fn submit<T: OpCode + 'static>(op: T) -> Submit<T> {
633    Runtime::with_current(|r| r.submit(op))
634}
635
636/// Submit a multishot operation to the current runtime, and return a stream for
637/// it.
638///
639/// ## Panics
640///
641/// This method doesn't create runtime and will panic if it's not within a
642/// runtime. It tries to obtain the current runtime with
643/// [`Runtime::with_current`].
644pub fn submit_multi<T: OpCode + 'static>(op: T) -> SubmitMulti<T> {
645    Runtime::with_current(|r| r.submit_multi(op))
646}
647
648/// Register file descriptors for fixed-file operations with the current
649/// runtime's io_uring instance.
650///
651/// This only works on `io_uring` driver. It will return an [`Unsupported`]
652/// error on other drivers.
653///
654/// ## Panics
655///
656/// This method doesn't create runtime. It tries to obtain the current runtime
657/// by [`Runtime::with_current`].
658///
659/// [`Unsupported`]: std::io::ErrorKind::Unsupported
660pub fn register_files(fds: &[RawFd]) -> io::Result<()> {
661    Runtime::with_current(|r| r.register_files(fds))
662}
663
664/// Unregister previously registered file descriptors from the current
665/// runtime's io_uring instance.
666///
667/// This only works on `io_uring` driver. It will return an [`Unsupported`]
668/// error on other drivers.
669///
670/// ## Panics
671///
672/// This method doesn't create runtime. It tries to obtain the current runtime
673/// by [`Runtime::with_current`].
674///
675/// [`Unsupported`]: std::io::ErrorKind::Unsupported
676pub fn unregister_files() -> io::Result<()> {
677    Runtime::with_current(|r| r.unregister_files())
678}