use core::future::Future;
use std::fmt;
use std::io;
use std::sync::{Arc, OnceLock};
use std::thread::{JoinHandle, ThreadId};
use std::time::Duration;
use kovan_channel::flavors::RecvDeadline;
use super::blocking::Pool;
use super::context;
use super::handoff::life::Life;
use super::sched::Sched;
use super::shard::{Shard, Shared};
use super::task::{self, ScheduleInfo, Task};
use super::workers::{TEARDOWN_WAIT, Workers};
mod builder;
mod cores;
mod enter;
mod fallible;
pub(crate) mod io_knobs;
mod knobs;
mod panic;
mod placement;
mod stats;
pub use self::builder::Builder;
pub use self::enter::{EnterGuard, RuntimeId, TryCurrentError, worker_index};
pub(crate) use self::fallible::{try_current, try_spawn, try_spawn_blocking, try_spawn_local};
pub use super::rng::RngSeed;
pub use super::tuning::{TaskMeta, UnhandledPanic};
pub(crate) struct Inner {
id: u64,
pub(crate) workers: Workers,
pub(crate) pool: Arc<Pool>,
local_margin: usize,
#[cfg(ring)]
rings: bool,
pub(crate) clock: crate::time::clock::Clock,
guests: OnceLock<kovan_map::HashMap<usize, Arc<Shared>>>,
task_hooks: Option<Arc<super::tuning::TaskHooks>>,
shutdown_on_panic: bool,
panicked: std::sync::atomic::AtomicBool,
on_panic: crate::sync::Notify,
rng_seed: Option<RngSeed>,
lingering: super::linger::Lingering,
pub(crate) io: io_knobs::IoKnobs,
pub(crate) buffer_cap: usize,
}
pub struct Runtime {
inner: Arc<Inner>,
exits: kovan_channel::unbounded::Receiver<ThreadId>,
}
#[derive(Clone)]
pub struct RuntimeHandle {
inner: Arc<Inner>,
}
impl Runtime {
pub fn builder() -> Builder {
Builder::new()
}
pub fn new() -> io::Result<Self> {
Builder::new().build()
}
pub fn handle(&self) -> RuntimeHandle {
RuntimeHandle {
inner: Arc::clone(&self.inner),
}
}
pub fn block_on<F: Future>(&self, future: F) -> F::Output {
block_on_with(&self.inner, future)
}
pub fn spawn<F>(&self, future: F) -> Task<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
spawn_with(&self.inner, future)
}
pub fn spawn_blocking<F, T>(&self, f: F) -> Task<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
self.inner.pool.spawn(f)
}
pub fn shutdown_timeout(mut self, timeout: Duration) {
let deadline = std::time::Instant::now() + timeout;
self.stop_and_join(Some(timeout));
self.finish_lingering(Some(deadline));
}
fn finish_lingering(&self, deadline: Option<std::time::Instant>) {
if self.inner.lingering.is_over() {
return;
}
if !self.inner.lingers_done() {
let linger = std::time::Instant::now() + self.inner.io.linger;
let deadline = deadline.map_or(linger, |d| d.min(linger));
let _ = block_on_here(
&self.inner,
crate::time::timeout_at(deadline, self.inner.run_lingers()),
);
}
self.inner.lingering.end();
}
fn stop_and_join(&mut self, timeout: Option<Duration>) {
let now = std::time::Instant::now();
let deadline = timeout.map(|timeout| now + timeout);
self.inner
.workers
.set_teardown_by(now + timeout.unwrap_or(TEARDOWN_WAIT));
self.inner.lingering.close();
self.inner.workers.stop_all();
let mut exited = Vec::new();
if let Some(monitor) = self.inner.workers.take_monitor() {
self.join(vec![monitor], deadline, &mut exited);
}
#[cfg(test)]
teardown_pause(false);
let taken = self.inner.workers.take_threads();
#[cfg(test)]
teardown_pause(true);
self.join(taken.joined, deadline, &mut exited);
self.join_detached(taken.detached, &mut exited);
}
fn join_detached(&self, threads: Vec<(JoinHandle<()>, Arc<Life>)>, exited: &mut Vec<ThreadId>) {
if threads.is_empty() {
return;
}
let (handles, lives): (Vec<_>, Vec<_>) = threads.into_iter().unzip();
self.join(handles, Some(self.inner.workers.teardown_by()), exited);
let still = lives.iter().filter(|life| life.is_detached()).count();
if still != 0 {
log::warn!(
"{still} runtime threads were still in a task's poll at the shutdown deadline; the tasks they hold end when those polls return"
);
}
}
fn join(
&self,
threads: Vec<JoinHandle<()>>,
deadline: Option<std::time::Instant>,
exited: &mut Vec<ThreadId>,
) {
let me = std::thread::current().id();
let threads = threads
.into_iter()
.filter(|thread| thread.thread().id() != me);
let Some(deadline) = deadline else {
for thread in threads {
let _ = thread.join();
}
return;
};
let threads: Vec<_> = threads.collect();
while threads
.iter()
.any(|thread| !thread.is_finished() && !exited.contains(&thread.thread().id()))
{
match self.exits.recv_deadline(deadline) {
RecvDeadline::Msg(id) => exited.push(id),
_ => break,
}
}
for thread in threads {
if thread.is_finished() {
let _ = thread.join();
}
}
}
}
#[cfg(test)]
impl Runtime {
pub(crate) fn inner(&self) -> &Arc<Inner> {
&self.inner
}
}
#[cfg(test)]
type Pause = (bool, Box<dyn FnOnce()>);
#[cfg(test)]
thread_local! {
pub(crate) static TEARDOWN_PAUSE: core::cell::RefCell<Option<Pause>> =
const { core::cell::RefCell::new(None) };
}
#[cfg(test)]
fn teardown_pause(after_take: bool) {
let pause =
TEARDOWN_PAUSE.with(|pause| pause.borrow_mut().take_if(|(at, _)| *at == after_take));
if let Some((_, run)) = pause {
run();
}
}
#[cfg(all(test, not(loom)))]
mod tests_drop;
#[cfg(all(test, not(loom)))]
mod tests_nested;
impl Drop for Runtime {
fn drop(&mut self) {
self.stop_and_join(None);
self.finish_lingering(None);
}
}
impl fmt::Debug for Runtime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Runtime")
.field("worker_threads", &self.inner.workers.running())
.finish_non_exhaustive()
}
}
impl RuntimeHandle {
pub fn block_on<F: Future>(&self, future: F) -> F::Output {
block_on_with(&self.inner, future)
}
pub fn spawn<F>(&self, future: F) -> Task<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
spawn_with(&self.inner, future)
}
pub fn spawn_blocking<F, T>(&self, f: F) -> Task<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
self.inner.pool.spawn(f)
}
pub fn current() -> Self {
Self { inner: current() }
}
pub(crate) fn inner(&self) -> &Arc<Inner> {
&self.inner
}
}
impl fmt::Debug for RuntimeHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RuntimeHandle")
.field("worker_threads", &self.inner.workers.running())
.finish_non_exhaustive()
}
}
static DEFAULT: OnceLock<Runtime> = OnceLock::new();
impl Inner {
pub(crate) fn guests(&self) -> &kovan_map::HashMap<usize, Arc<Shared>> {
self.guests
.get_or_init(|| kovan_map::HashMap::with_capacity(builder::GUESTS_CAPACITY))
}
pub(crate) fn guests_made(&self) -> Option<&kovan_map::HashMap<usize, Arc<Shared>>> {
self.guests.get()
}
pub(crate) fn id(&self) -> u64 {
self.id
}
pub(crate) fn lingering(&self) -> &super::linger::Lingering {
&self.lingering
}
}
#[expect(
clippy::panic,
reason = "no caller to hand the error to; documented under `# Panics`"
)]
pub(crate) fn default_runtime() -> &'static Runtime {
fallible::try_default()
.unwrap_or_else(|err| panic!("the default runtime could not start: {err}"))
}
impl Inner {
fn arc(&self) -> Arc<Inner> {
unsafe {
Arc::increment_strong_count(self);
Arc::from_raw(self)
}
}
}
pub(crate) fn current() -> Arc<Inner> {
context::runtime().unwrap_or_else(|| Arc::clone(&default_runtime().inner))
}
pub(crate) fn with_clock<R>(f: impl FnOnce(Option<&crate::time::clock::Clock>) -> R) -> R {
match context::runtime() {
Some(inner) => f(Some(&inner.clock)),
None => f(DEFAULT.get().map(|rt| &rt.inner.clock)),
}
}
fn running() -> Option<Arc<Inner>> {
context::runtime().or_else(|| DEFAULT.get().map(|rt| Arc::clone(&rt.inner)))
}
pub(crate) fn running_inner() -> Option<Arc<Inner>> {
running()
}
pub(crate) fn exit_linger() -> std::time::Duration {
DEFAULT
.get()
.map_or(crate::io::handle::LINGER, |rt| rt.inner.io.linger)
}
pub(crate) fn spawn_linger(inner: &Arc<Inner>, linger: super::linger::Guarded) {
match inner.workers.place(inner.id, inner.local_margin) {
Some(shard) => spawn_on(inner, shard, linger).detach(),
None => super::linger::on_thread(linger),
}
}
pub(crate) fn running_service_shard() -> Option<Arc<Shared>> {
let inner = running()?;
inner
.workers
.place(inner.id, inner.local_margin)
.map(Arc::clone)
}
#[cfg(ring)]
pub(crate) fn service_rings() -> bool {
let inner = current();
if inner.workers.running() > 0 {
inner.rings
} else {
super::backend::chosen().is_ok_and(|b| b.rings())
}
}
pub(crate) fn service_shard() -> Arc<Shared> {
let inner = current();
match inner.workers.place(inner.id, inner.local_margin) {
Some(shard) => Arc::clone(shard),
None => Arc::clone(&own_shard().shared),
}
}
fn own_shard() -> std::rc::Rc<Shard> {
context::own_shard_if_made().unwrap_or_else(start_own_shard)
}
#[cold]
#[inline(never)]
#[expect(
clippy::panic,
reason = "no caller to hand the error to; documented under `# Panics`"
)]
fn start_own_shard() -> std::rc::Rc<Shard> {
match context::own_shard() {
Ok(shard) => shard,
Err(err) => panic!("this thread's shard could not start: {err}"),
}
}
#[inline(always)]
fn spawn_with<F>(inner: &Inner, future: F) -> Task<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let Some(shard) = inner.workers.place(inner.id, inner.local_margin) else {
return spawn_unworked(inner, future);
};
spawn_on(inner, shard, future)
}
fn spawn_unworked<F>(inner: &Inner, future: F) -> Task<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let hooks =
if context::current(|current| current.is_some_and(|shard| shard.serves_other(inner))) {
Some(visitor_hooks(inner))
} else {
context_hooks()
};
spawn_here::<_, false>(future, hooks)
}
#[cold]
#[inline(never)]
fn visitor_hooks(inner: &Inner) -> Arc<super::tuning::TaskHooks> {
let mut hooks = inner.task_hooks.as_deref().cloned().unwrap_or_default();
hooks.visiting = Some(Arc::new(super::sched::Visiting::new(inner.arc())));
Arc::new(hooks)
}
#[inline(always)]
fn spawn_on<F>(inner: &Inner, shard: &Arc<Shared>, future: F) -> Task<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let hooks = inner.task_hooks.as_ref();
let (runnable, task) = task::spawn(
future,
Sched::<false>::counted(Arc::clone(shard), None, hooks.cloned()),
);
spawned(hooks.map(|hooks| &**hooks), &task);
if let Some(runnable) = super::sched::queue_here(shard, runnable, ScheduleInfo::new(false)) {
runnable.schedule();
}
task
}
#[inline(always)]
fn spawned<T>(hooks: Option<&super::tuning::TaskHooks>, task: &Task<T>) {
if let Some(hooks) = hooks {
spawned_with(hooks, task.id());
}
}
#[cold]
#[inline(never)]
fn spawned_with(hooks: &super::tuning::TaskHooks, id: task::Id) {
crate::task::name_now(id);
super::tuning::call(hooks.spawn.as_ref(), id);
}
fn context_hooks() -> Option<Arc<super::tuning::TaskHooks>> {
if !super::tuning::ANY_TASK_HOOKS.load(std::sync::atomic::Ordering::Acquire) {
return None;
}
context::runtime().and_then(|inner| inner.task_hooks.clone())
}
#[cold]
#[inline(never)]
fn lost_spawn(bound: bool) -> Option<Arc<Shared>> {
if bound {
return None;
}
context::lost_shared(|lost| lost.map(Arc::clone))
}
fn spawn_here<F, const BOUND: bool>(
future: F,
hooks: Option<Arc<super::tuning::TaskHooks>>,
) -> Task<F::Output>
where
F: Future + 'static,
F::Output: 'static,
{
let place = |shard: &Shard| {
(
Arc::clone(&shard.shared),
BOUND.then(|| context::home_for(shard)),
)
};
let (shared, home) =
context::current(|current| current.map(place)).unwrap_or_else(|| match lost_spawn(BOUND) {
Some(lost) => (lost, None),
None => place(&own_shard()),
});
let at = home.as_ref().map(Arc::as_ptr);
let (runnable, task, bound_to) = if BOUND {
let (runnable, task) = unsafe {
task::spawn_unchecked(future, Sched::<true>::counted(shared, home, hooks.clone()))
};
if let Some(at) = at {
unsafe { (*at).push_task(runnable.links()) };
}
(runnable, task, at)
} else {
let sched = match &hooks {
Some(visitor) if visitor.visiting.is_some() => {
Sched::<false>::visitor(shared, Arc::clone(visitor))
}
_ => Sched::<false>::counted(shared, None, hooks.clone()),
};
let (runnable, task) = unsafe { task::spawn_unchecked(future, sched) };
(runnable, task, None)
};
spawned(hooks.as_deref(), &task);
let left = match bound_to {
Some(home) => {
let home = unsafe { &*home };
super::sched::queue_home_here(home, runnable, ScheduleInfo::new(false))
}
None => Some(runnable),
};
if let Some(runnable) = left {
runnable.schedule();
}
task
}
fn block_on_with<F: Future>(inner: &Arc<Inner>, future: F) -> F::Output {
if inner.shutdown_on_panic {
return block_on_here(inner, panic::watched(inner, future));
}
block_on_here(inner, future)
}
fn block_on_here<F: Future>(inner: &Arc<Inner>, future: F) -> F::Output {
let runtime = context::set_runtime(inner);
let mut future = core::pin::pin!(future);
if let Some(output) = context::current(|current| {
current.map(|shard| shard.block_on_nested(future.as_mut(), runtime.previous(), inner))
}) {
return output;
}
let shard = own_shard();
let _current = context::enter(&shard);
let _guest = super::paused::Guest::enter(inner, &shard.shared);
let _seeded = inner
.rng_seed
.map(|seed| super::rng::Reseeded::new(seed, 0));
#[cfg(port)]
shard.take_port_back();
shard.block_on_then_leave(future.as_mut())
}
pub fn block_on<F: Future>(future: F) -> F::Output {
block_on_with(¤t(), future)
}
#[inline]
pub fn spawn<F>(future: F) -> Task<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let spawned = context::with_runtime(|inner| match inner {
Some(inner) if inner.task_hooks.is_none() => Ok(spawn_with(inner, future)),
_ => Err(future),
});
spawned.unwrap_or_else(spawn_current)
}
#[inline(never)]
fn spawn_current<F>(future: F) -> Task<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
spawn_with(¤t(), future)
}
pub fn spawn_local<F>(future: F) -> Task<F::Output>
where
F: Future + 'static,
F::Output: 'static,
{
spawn_here::<_, true>(future, context_hooks())
}
pub fn spawn_blocking<F, T>(f: F) -> Task<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
current().pool.spawn(f)
}
#[cfg(unix)]
pub(crate) fn spawn_service<F>(future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let shared = context::current(|current| current.map(|s| Arc::clone(&s.shared)))
.unwrap_or_else(service_shard);
let (runnable, task) = unsafe {
task::spawn_unchecked(future, Sched::<false>::uncounted(shared))
};
runnable.schedule();
task.detach();
}
#[cfg(port)]
pub(crate) fn spawn_blocking_inside(f: impl FnOnce() + Send + 'static) -> bool {
match blocking_pool_inside() {
Some(pool) => {
pool.spawn(f).detach();
true
}
None => false,
}
}
#[cfg(completion)]
pub(crate) fn blocking_pool_inside() -> Option<Arc<super::blocking::Pool>> {
context::runtime().map(|inner| Arc::clone(&inner.pool))
}
pub(crate) fn allowed_cores() -> Vec<usize> {
#[cfg(target_os = "linux")]
{
current_mask().map_or_else(Vec::new, |set| {
(0..libc::CPU_SETSIZE as usize)
.filter(|&cpu| unsafe { libc::CPU_ISSET(cpu, &set) })
.collect()
})
}
#[cfg(windows)]
{
use crate::syscore::windows::{GetCurrentProcess, GetProcessAffinityMask};
let (mut process, mut system) = (0usize, 0usize);
if unsafe { GetProcessAffinityMask(GetCurrentProcess(), &mut process, &mut system) } != 0 {
return (0..usize::BITS as usize)
.filter(|&cpu| process & (1 << cpu) != 0)
.collect();
}
Vec::new()
}
#[cfg(not(any(target_os = "linux", windows)))]
{
Vec::new()
}
}
#[cfg(target_os = "linux")]
pub(crate) type CpuMask = libc::cpu_set_t;
#[cfg(windows)]
pub(crate) type CpuMask = usize;
#[cfg(not(any(target_os = "linux", windows)))]
pub(crate) type CpuMask = ();
pub(crate) fn current_mask() -> Option<CpuMask> {
#[cfg(target_os = "linux")]
{
unsafe {
let mut set: libc::cpu_set_t = core::mem::zeroed();
(libc::sched_getaffinity(0, size_of::<libc::cpu_set_t>(), &mut set) == 0).then_some(set)
}
}
#[cfg(not(target_os = "linux"))]
None
}
pub(crate) fn apply_mask(mask: &CpuMask) {
#[cfg(target_os = "linux")]
let _ = unsafe { libc::sched_setaffinity(0, size_of::<libc::cpu_set_t>(), mask) };
#[cfg(windows)]
{
use crate::syscore::windows::{GetCurrentThread, SetThreadAffinityMask};
unsafe { SetThreadAffinityMask(GetCurrentThread(), *mask) };
}
#[cfg(not(any(target_os = "linux", windows)))]
let _ = mask;
}
pub(crate) fn pin_to(core: usize) {
#[cfg(target_os = "linux")]
unsafe {
let mut set: libc::cpu_set_t = core::mem::zeroed();
libc::CPU_SET(core, &mut set);
let _ = libc::sched_setaffinity(0, size_of::<libc::cpu_set_t>(), &set);
}
#[cfg(windows)]
if core < usize::BITS as usize {
use crate::syscore::windows::{GetCurrentThread, SetThreadAffinityMask};
unsafe { SetThreadAffinityMask(GetCurrentThread(), 1 << core) };
}
#[cfg(not(any(target_os = "linux", windows)))]
let _ = core;
}