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}