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}