use core::future::Future;
use std::fmt;
use std::io;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use kovan_channel::flavors::RecvDeadline;
use super::blocking::{Pool, PoolConfig};
use super::context;
use super::sched::{AtHome, Counted, Sched};
use super::shard::{Limits, Shard, Shared};
use super::task::{self, Task};
use super::workers::{Start, Workers};
mod enter;
mod knobs;
mod panic;
pub use self::enter::{EnterGuard, RuntimeId, TryCurrentError, worker_index};
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,
pub(crate) guests: 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>,
}
#[must_use]
pub struct Builder {
worker_threads: Option<usize>,
thread_name: Option<Arc<dyn Fn() -> String + Send + Sync>>,
thread_stack_size: Option<usize>,
max_nesting: Option<usize>,
max_blocking_threads: Option<usize>,
blocking_keep_alive: Option<Duration>,
pin_workers: Option<bool>,
local_spawn_margin: Option<usize>,
io_buffer_cap: Option<usize>,
max_worker_threads: Option<usize>,
backend: Option<crate::IoBackend>,
handoff_after: Option<Option<Duration>>,
start_paused: bool,
tuning: super::tuning::Tuning,
task_hooks: super::tuning::TaskHooks,
unhandled_panic: UnhandledPanic,
rng_seed: Option<RngSeed>,
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}
const DEFAULT_STACK: usize = 8 * 1024 * 1024;
const DEFAULT_BLOCKING_THREADS: usize = 512;
const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(10);
const DEFAULT_LOCAL_MARGIN: usize = 32;
const DEFAULT_HANDOFF: Duration = Duration::from_millis(10);
impl Builder {
pub fn new() -> Self {
Self {
worker_threads: None,
thread_name: None,
thread_stack_size: None,
max_nesting: None,
max_blocking_threads: None,
blocking_keep_alive: None,
pin_workers: None,
local_spawn_margin: None,
io_buffer_cap: None,
max_worker_threads: None,
backend: None,
handoff_after: None,
start_paused: false,
tuning: super::tuning::Tuning::default(),
task_hooks: super::tuning::TaskHooks::default(),
unhandled_panic: UnhandledPanic::Ignore,
rng_seed: None,
}
}
pub fn worker_threads(mut self, n: usize) -> Self {
self.worker_threads = Some(n);
self
}
pub fn thread_name(mut self, name: impl Into<String>) -> Self {
let name: String = name.into();
self.thread_name = Some(Arc::new(move || name.clone()));
self
}
pub fn thread_name_fn(mut self, f: impl Fn() -> String + Send + Sync + 'static) -> Self {
self.thread_name = Some(Arc::new(f));
self
}
pub fn thread_stack_size(mut self, bytes: usize) -> Self {
self.thread_stack_size = Some(bytes);
self
}
pub fn max_nesting(mut self, depth: usize) -> Self {
self.max_nesting = Some(depth);
self
}
pub fn max_blocking_threads(mut self, n: usize) -> Self {
self.max_blocking_threads = Some(n);
self
}
pub fn thread_keep_alive(mut self, keep_alive: Duration) -> Self {
self.blocking_keep_alive = Some(keep_alive);
self
}
pub fn local_spawn_margin(mut self, margin: usize) -> Self {
self.local_spawn_margin = Some(margin);
self
}
pub fn max_worker_threads(mut self, n: usize) -> Self {
self.max_worker_threads = Some(n);
self
}
pub fn io_buffer_cap(mut self, bytes: usize) -> Self {
self.io_buffer_cap = Some(bytes);
self
}
pub fn backend(mut self, backend: crate::IoBackend) -> Self {
self.backend = Some(backend);
self
}
pub fn start_paused(mut self, paused: bool) -> Self {
self.start_paused = paused;
self
}
pub fn handoff_after(mut self, after: Option<Duration>) -> Self {
self.handoff_after = Some(after);
self
}
pub fn pin_workers(mut self, pin: bool) -> Self {
self.pin_workers = Some(pin);
self
}
pub fn build(self) -> io::Result<Runtime> {
let workers = self
.worker_threads
.unwrap_or_else(|| std::thread::available_parallelism().map_or(1, usize::from));
let stack = self.thread_stack_size.unwrap_or(DEFAULT_STACK);
let limits = Limits {
max_nesting: self.max_nesting.unwrap_or(context::DEFAULT_MAX_NESTING),
buffer_cap: self.io_buffer_cap.unwrap_or(crate::buf::DEFAULT_CAP),
backend: super::backend::for_runtime(self.backend)?,
};
#[cfg(ring)]
let rings = limits.backend.rings();
let blocking_name = self
.thread_name
.clone()
.unwrap_or_else(|| Arc::new(|| "nuclei-blocking".to_string()));
let worker_name = self
.thread_name
.unwrap_or_else(|| Arc::new(|| "nuclei-worker".to_string()));
let pool = Pool::new(PoolConfig {
hooks: self.tuning.hooks.clone(),
max_threads: self
.max_blocking_threads
.unwrap_or(DEFAULT_BLOCKING_THREADS)
.max(1),
keep_alive: self.blocking_keep_alive.unwrap_or(DEFAULT_KEEP_ALIVE),
stack_size: self.thread_stack_size,
name: blocking_name,
});
static IDS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let (exit_tx, exits) = kovan_channel::unbounded();
let cores = if self.pin_workers.unwrap_or(true) {
allowed_cores()
} else {
Vec::new()
};
let capacity = self
.max_worker_threads
.unwrap_or(workers.saturating_mul(4))
.max(workers);
let shutdown_on_panic = self.unhandled_panic == UnhandledPanic::ShutdownRuntime;
let mut task_hooks = self.task_hooks;
if task_hooks.before_poll.is_some() || task_hooks.after_poll.is_some() {
super::tuning::ANY_POLL_HOOKS.store(true, std::sync::atomic::Ordering::Release);
}
let inner = Arc::new_cyclic(|this| {
if shutdown_on_panic {
task_hooks.shutdown = Some(this.clone());
}
Inner {
id: IDS.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
workers: Workers::new(
capacity,
workers,
Start {
limits,
tuning: self.tuning.clone(),
seed: self.rng_seed,
stack,
name: worker_name,
cores,
handoff_after: self.handoff_after.unwrap_or(Some(DEFAULT_HANDOFF)),
},
exit_tx,
),
pool,
local_margin: self.local_spawn_margin.unwrap_or(DEFAULT_LOCAL_MARGIN),
#[cfg(ring)]
rings,
clock: crate::time::clock::Clock::new(self.start_paused),
task_hooks: task_hooks.any().then(|| {
super::tuning::ANY_TASK_HOOKS.store(true, std::sync::atomic::Ordering::Release);
Arc::new(task_hooks)
}),
shutdown_on_panic,
panicked: std::sync::atomic::AtomicBool::new(false),
on_panic: crate::sync::Notify::new(),
rng_seed: self.rng_seed,
guests: kovan_map::HashMap::new(),
}
});
inner.pool.set_owner(Arc::downgrade(&inner));
let runtime = Runtime { inner, exits };
Workers::start(&runtime.inner, workers)?;
Ok(runtime)
}
pub fn build_global(self) -> io::Result<RuntimeHandle> {
let runtime = self.build()?;
let handle = runtime.handle();
DEFAULT
.set(runtime)
.map_err(|_| io::Error::other("the default runtime is already running"))?;
Ok(handle)
}
}
impl fmt::Debug for Builder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Builder")
.field("worker_threads", &self.worker_threads)
.field("thread_stack_size", &self.thread_stack_size)
.field("max_nesting", &self.max_nesting)
.field("max_blocking_threads", &self.max_blocking_threads)
.field("thread_keep_alive", &self.blocking_keep_alive)
.field("pin_workers", &self.pin_workers)
.field("local_spawn_margin", &self.local_spawn_margin)
.field("io_buffer_cap", &self.io_buffer_cap)
.field("start_paused", &self.start_paused)
.finish_non_exhaustive()
}
}
pub struct Runtime {
inner: Arc<Inner>,
exits: kovan_channel::unbounded::Receiver<()>,
}
#[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) {
self.stop_and_join(Some(timeout));
}
fn stop_and_join(&mut self, timeout: Option<Duration>) {
self.inner.workers.stop_all();
let threads = self.inner.workers.take_threads();
if let Some(timeout) = timeout {
let deadline = std::time::Instant::now() + timeout;
for thread in &threads {
if thread.is_finished() {
continue;
}
if !matches!(self.exits.recv_deadline(deadline), RecvDeadline::Msg(())) {
break;
}
}
for thread in threads {
if thread.is_finished() {
let _ = thread.join();
}
}
} else {
for thread in threads {
let _ = thread.join();
}
}
}
}
#[cfg(test)]
impl Runtime {
pub(crate) fn inner(&self) -> &Arc<Inner> {
&self.inner
}
}
impl Drop for Runtime {
fn drop(&mut self) {
self.stop_and_join(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() }
}
}
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 id(&self) -> u64 {
self.id
}
}
#[expect(
clippy::panic,
reason = "no caller to hand the error to; documented under `# Panics`"
)]
pub(crate) fn default_runtime() -> &'static Runtime {
DEFAULT.get_or_init(|| {
Builder::new()
.build()
.unwrap_or_else(|err| panic!("the default runtime could not start: {err}"))
})
}
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)),
}
}
pub(crate) fn running_service_shard() -> Option<Arc<Shared>> {
let inner = context::runtime().or_else(|| DEFAULT.get().map(|rt| Arc::clone(&rt.inner)))?;
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),
}
}
#[expect(
clippy::panic,
reason = "no caller to hand the error to; documented under `# Panics`"
)]
fn own_shard() -> std::rc::Rc<Shard> {
match context::own_shard() {
Ok(shard) => shard,
Err(err) => panic!("this thread's shard could not start: {err}"),
}
}
fn spawn_with<F>(inner: &Arc<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_here(future, false);
};
let hooks = inner.task_hooks.clone();
let (runnable, task) = task::spawn(
Counted::new(future, shard),
Sched {
shard: Arc::clone(shard),
home: None,
hooks: hooks.clone(),
},
);
spawned(hooks.as_deref(), &task);
runnable.schedule();
task
}
fn spawned<T>(hooks: Option<&super::tuning::TaskHooks>, task: &Task<T>) {
if let Some(hooks) = hooks {
super::tuning::call(hooks.spawn.as_ref(), task.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())
}
fn spawn_here<F>(future: F, bound: bool) -> 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(|| place(&own_shard()));
let hooks = context_hooks();
let (runnable, task) = if let Some(home) = home {
unsafe {
task::spawn_unchecked(
Counted::new(AtHome::new(future, &home), &shared),
Sched {
shard: shared,
home: Some(home),
hooks: hooks.clone(),
},
)
}
} else {
unsafe {
task::spawn_unchecked(
Counted::new(future, &shared),
Sched {
shard: shared,
home: None,
hooks: hooks.clone(),
},
)
}
};
spawned(hooks.as_deref(), &task);
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(future.as_mut())))
{
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)
}
pub fn spawn<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(future, true)
}
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 {
shard: shared,
home: None,
hooks: None,
},
)
};
runnable.schedule();
task.detach();
}
#[cfg(port)]
pub(crate) fn spawn_blocking_inside(f: impl FnOnce() + Send + 'static) -> bool {
match context::runtime() {
Some(inner) => {
inner.pool.spawn(f).detach();
true
}
None => false,
}
}
pub(crate) fn allowed_cores() -> Vec<usize> {
#[cfg(target_os = "linux")]
{
unsafe {
let mut set: libc::cpu_set_t = core::mem::zeroed();
if libc::sched_getaffinity(0, size_of::<libc::cpu_set_t>(), &mut set) == 0 {
return (0..libc::CPU_SETSIZE as usize)
.filter(|&cpu| libc::CPU_ISSET(cpu, &set))
.collect();
}
}
Vec::new()
}
#[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()
}
}
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;
}