use std::sync::{Arc, OnceLock};
use std::thread;
use tokio::runtime::{Builder, Handle, Runtime};
pub const MAX_RUNTIME_THREADS: usize = 1024;
fn default_runtime_threads() -> usize {
thread::available_parallelism()
.map_or(1, |n| n.get())
.min(MAX_RUNTIME_THREADS)
}
struct OwnedRuntime(Option<Runtime>);
impl Drop for OwnedRuntime {
fn drop(&mut self) {
if let Some(runtime) = self.0.take() {
runtime.shutdown_background();
}
}
}
#[derive(Clone)]
pub(crate) struct LoaderRuntime {
pub(super) handle: Handle,
_owned: Option<Arc<OwnedRuntime>>,
}
impl LoaderRuntime {
fn own(threads: usize) -> Self {
let runtime = Builder::new_multi_thread()
.worker_threads(threads)
.thread_name("dataloader-io")
.enable_all()
.build()
.expect("failed to build the loader's tokio runtime");
Self {
handle: runtime.handle().clone(),
_owned: Some(Arc::new(OwnedRuntime(Some(runtime)))),
}
}
}
enum RuntimeSetting {
Own(Option<usize>),
Caller(Handle),
}
pub(crate) struct LazyRuntime {
setting: RuntimeSetting,
runtime: OnceLock<LoaderRuntime>,
}
impl LazyRuntime {
pub(crate) fn new() -> Self {
Self {
setting: RuntimeSetting::Own(None),
runtime: OnceLock::new(),
}
}
pub(crate) fn set_handle(&mut self, handle: Handle) {
assert!(
!matches!(self.setting, RuntimeSetting::Own(Some(_))),
"runtime and runtime_threads are mutually exclusive: \
runtime_threads sizes the loader's own runtime"
);
self.setting = RuntimeSetting::Caller(handle);
}
pub(crate) fn set_threads(&mut self, threads: usize) {
assert!(
(1..=MAX_RUNTIME_THREADS).contains(&threads),
"runtime_threads must be between 1 and {MAX_RUNTIME_THREADS}, got {threads}"
);
assert!(
matches!(self.setting, RuntimeSetting::Own(_)),
"runtime and runtime_threads are mutually exclusive: \
runtime_threads sizes the loader's own runtime"
);
self.setting = RuntimeSetting::Own(Some(threads));
}
pub(crate) fn get(&self) -> &LoaderRuntime {
self.runtime.get_or_init(|| match &self.setting {
RuntimeSetting::Own(threads) => {
LoaderRuntime::own(threads.unwrap_or_else(default_runtime_threads))
}
RuntimeSetting::Caller(handle) => LoaderRuntime {
handle: handle.clone(),
_owned: None,
},
})
}
}
#[cfg(test)]
mod tests {
use std::thread;
use super::{LazyRuntime, MAX_RUNTIME_THREADS};
fn workers(runtime: &LazyRuntime) -> usize {
runtime.get().handle.metrics().num_workers()
}
#[test]
fn default_runtime_threads_are_the_available_cpus() {
let expected = thread::available_parallelism()
.map_or(1, |n| n.get())
.min(MAX_RUNTIME_THREADS);
assert_eq!(workers(&LazyRuntime::new()), expected);
}
#[test]
fn runtime_threads_sizes_the_runtime() {
let mut runtime = LazyRuntime::new();
runtime.set_threads(3);
assert_eq!(workers(&runtime), 3);
}
}