nuclei/rt/runtime/builder.rs
1//! The [`Builder`]: every setting of a runtime, checked once in
2//! [`build`](Builder::build), which starts the runtime they describe.
3//!
4//! A setter only records its value; `build` checks them all, each against
5//! what a runtime can work with and against the others, and refuses the
6//! first one that does not fit with `InvalidInput` naming it. No setter
7//! panics.
8
9use std::fmt;
10use std::io;
11use std::sync::Arc;
12use std::time::Duration;
13
14use super::super::blocking::{Pool, PoolConfig};
15use super::super::context;
16use super::super::shard::Limits;
17use super::super::workers::{Start, Workers};
18use super::{DEFAULT, Inner, RngSeed, Runtime, RuntimeHandle, UnhandledPanic};
19
20/// Builds a [`Runtime`]. Every setting is optional: an unset one takes the
21/// default, which needs no configuration to work well.
22///
23/// ```
24/// let runtime = nuclei::Builder::new().worker_threads(2).build().unwrap();
25/// assert_eq!(runtime.block_on(async { 1 + 1 }), 2);
26/// ```
27#[must_use]
28pub struct Builder {
29 pub(super) worker_threads: Option<usize>,
30 pub(super) thread_name: Option<Arc<dyn Fn() -> String + Send + Sync>>,
31 pub(super) thread_stack_size: Option<usize>,
32 pub(super) max_nesting: Option<usize>,
33 pub(super) max_blocking_threads: Option<usize>,
34 pub(super) blocking_keep_alive: Option<Duration>,
35 pub(super) pin_workers: Option<bool>,
36 pub(super) local_spawn_margin: Option<usize>,
37 pub(super) io_buffer_cap: Option<usize>,
38 pub(super) max_worker_threads: Option<usize>,
39 pub(super) backend: Option<crate::IoBackend>,
40 pub(super) io_uring: Option<crate::config::IoUringConfiguration>,
41 pub(super) worker_cores: Option<Vec<usize>>,
42 pub(super) blocking_cores: Option<Vec<usize>>,
43 pub(super) handoff_after: Option<Option<Duration>>,
44 pub(super) start_paused: bool,
45 pub(super) record_latencies: bool,
46 #[cfg(feature = "io-stats")]
47 pub(super) count_io: bool,
48 pub(super) tuning: super::super::tuning::Tuning,
49 pub(super) task_hooks: super::super::tuning::TaskHooks,
50 pub(super) long_poll: Option<super::super::tuning::TaskHook>,
51 pub(super) unhandled_panic: UnhandledPanic,
52 pub(super) rng_seed: Option<RngSeed>,
53 pub(super) io: super::io_knobs::IoSettings,
54}
55
56impl Default for Builder {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62/// Worker thread stacks: deep enough for nested `block_on`.
63const DEFAULT_STACK: usize = 8 * 1024 * 1024;
64const DEFAULT_BLOCKING_THREADS: usize = 512;
65const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(10);
66/// See [`Builder::local_spawn_margin`].
67const DEFAULT_LOCAL_MARGIN: usize = 32;
68/// See [`Builder::handoff_after`].
69const DEFAULT_HANDOFF: Duration = Duration::from_millis(10);
70
71/// The smallest worker or blocking thread stack. A worker starting, a timer
72/// and a TCP connect take 77 KiB in a debug build and 64 KiB in a release
73/// one (the shard is built on the stack before it moves to the heap), on
74/// glibc 2.39 and 2.44 alike; this leaves room over the deepest of them.
75const MIN_STACK: usize = 128 * 1024;
76/// The shortest `handoff_after`: the watch measures in milliseconds.
77const MIN_HANDOFF: Duration = Duration::from_millis(1);
78/// The smallest table the guest map accepts; it grows past it on demand.
79pub(super) const GUESTS_CAPACITY: usize = 64;
80
81impl Builder {
82 /// Refuses a setting no runtime can work with, or one that does not fit
83 /// with another, as the configuration's mistake.
84 fn validate(&self) -> io::Result<()> {
85 use crate::config::refuse;
86 if let Some(bytes) = self.thread_stack_size.filter(|&b| b < MIN_STACK) {
87 return Err(refuse(
88 "thread_stack_size",
89 bytes,
90 format_args!("at least {MIN_STACK} bytes"),
91 ));
92 }
93 if let Some(depth) = self.max_nesting.filter(|&d| d < 1) {
94 return Err(refuse("max_nesting", depth, "at least 1"));
95 }
96 if let Some(n) = self.max_blocking_threads.filter(|&n| n < 1) {
97 return Err(refuse("max_blocking_threads", n, "at least 1"));
98 }
99 if let (Some(workers), Some(max)) = (self.worker_threads, self.max_worker_threads)
100 && max < workers
101 {
102 return Err(refuse(
103 "max_worker_threads",
104 max,
105 format_args!("at least worker_threads ({workers})"),
106 ));
107 }
108 let handoff = self.handoff_after.unwrap_or(Some(DEFAULT_HANDOFF));
109 if let Some(after) = handoff.filter(|&a| a < MIN_HANDOFF) {
110 return Err(refuse(
111 "handoff_after",
112 format_args!("{after:?}"),
113 format_args!("at least {MIN_HANDOFF:?}, or None"),
114 ));
115 }
116 if self.long_poll.is_some() && handoff.is_none() {
117 return Err(refuse(
118 "on_long_poll",
119 "set with handoff_after(None)",
120 "used with handoff_after set to a duration, which is what a long poll passes",
121 ));
122 }
123 let tuning = &self.tuning;
124 for (setting, runs) in [
125 ("event_interval", tuning.driver_interval),
126 ("global_queue_interval", tuning.inbox_interval),
127 ] {
128 if runs < 1 {
129 return Err(refuse(setting, runs, "at least 1"));
130 }
131 }
132 if tuning.max_io_events < 1 {
133 return Err(refuse("max_io_events_per_tick", 0, "at least 1"));
134 }
135 if let Some(cfg) = &self.io_uring {
136 cfg.validate()?;
137 }
138 super::cores::validate("worker_cores", self.worker_cores.as_deref())?;
139 super::cores::validate("blocking_cores", self.blocking_cores.as_deref())?;
140 self.io.validate()
141 }
142
143 /// The workers the runtime starts with: `worker_threads`, else one per
144 /// core of `worker_cores`, else one per core the process may run on;
145 /// an unset count is held to `max_worker_threads`.
146 fn worker_count(&self) -> usize {
147 if let Some(n) = self.worker_threads {
148 return n;
149 }
150 let default = self.worker_cores.as_ref().map_or_else(
151 || std::thread::available_parallelism().map_or(1, usize::from),
152 Vec::len,
153 );
154 self.max_worker_threads
155 .map_or(default, |max| default.min(max))
156 }
157
158 /// A builder with every setting at its default.
159 pub fn new() -> Self {
160 Self {
161 worker_threads: None,
162 thread_name: None,
163 thread_stack_size: None,
164 max_nesting: None,
165 max_blocking_threads: None,
166 blocking_keep_alive: None,
167 pin_workers: None,
168 local_spawn_margin: None,
169 io_buffer_cap: None,
170 max_worker_threads: None,
171 backend: None,
172 io_uring: None,
173 worker_cores: None,
174 blocking_cores: None,
175 handoff_after: None,
176 start_paused: false,
177 record_latencies: false,
178 #[cfg(feature = "io-stats")]
179 count_io: false,
180 tuning: super::super::tuning::Tuning::default(),
181 task_hooks: super::super::tuning::TaskHooks::default(),
182 long_poll: None,
183 unhandled_panic: UnhandledPanic::Ignore,
184 rng_seed: None,
185 io: super::io_knobs::IoSettings::default(),
186 }
187 }
188
189 /// Worker threads, each running one shard. The default is one per core
190 /// of [`worker_cores`](Self::worker_cores) when it is set, else one per
191 /// core the process may run on, held to
192 /// [`max_worker_threads`](Self::max_worker_threads) when that is set.
193 /// Zero runs every task on the thread that spawns it, while that thread
194 /// is inside `block_on`.
195 pub fn worker_threads(mut self, n: usize) -> Self {
196 self.worker_threads = Some(n);
197 self
198 }
199
200 /// The name of every thread the runtime starts (default `nuclei-worker`
201 /// and `nuclei-blocking`).
202 pub fn thread_name(mut self, name: impl Into<String>) -> Self {
203 let name: String = name.into();
204 self.thread_name = Some(Arc::new(move || name.clone()));
205 self
206 }
207
208 /// A function naming each thread the runtime starts.
209 pub fn thread_name_fn(mut self, f: impl Fn() -> String + Send + Sync + 'static) -> Self {
210 self.thread_name = Some(Arc::new(f));
211 self
212 }
213
214 /// The stack size of every thread the runtime starts, in bytes (default
215 /// 8 MiB). At least 128 KiB; [`build`](Self::build) fails with `InvalidInput`
216 /// below.
217 pub fn thread_stack_size(mut self, bytes: usize) -> Self {
218 self.thread_stack_size = Some(bytes);
219 self
220 }
221
222 /// How deep `block_on` may nest on one thread (default 256). A call one
223 /// level deeper panics instead of running the thread out of stack. At least
224 /// 1; [`build`](Self::build) fails with `InvalidInput` below.
225 pub fn max_nesting(mut self, depth: usize) -> Self {
226 self.max_nesting = Some(depth);
227 self
228 }
229
230 /// The most threads the blocking pool runs at once (default 512). At least 1;
231 /// [`build`](Self::build) fails with `InvalidInput` below.
232 pub fn max_blocking_threads(mut self, n: usize) -> Self {
233 self.max_blocking_threads = Some(n);
234 self
235 }
236
237 /// How long an idle blocking thread waits for work before it exits
238 /// (default 10 seconds).
239 pub fn thread_keep_alive(mut self, keep_alive: Duration) -> Self {
240 self.blocking_keep_alive = Some(keep_alive);
241 self
242 }
243
244 /// How many more live tasks than another shard a spawning thread's shard
245 /// may hold and still take a task it spawns (default 32). A task spawned
246 /// on a worker stays on it (so a task that waits for what it spawned
247 /// keeps it on its own thread), and a task spawned on any other thread
248 /// goes to that thread's home shard (where its spawns went last), unless
249 /// that shard holds that many more tasks than a shard picked at random;
250 /// then it goes to the other, which becomes the home. Zero balances
251 /// strictly; a larger margin keeps more work together.
252 pub fn local_spawn_margin(mut self, margin: usize) -> Self {
253 self.local_spawn_margin = Some(margin);
254 self
255 }
256
257 /// The most worker threads the runtime may start over its life, the
258 /// ones it starts with included (default four times the workers it
259 /// starts with): what `spawn_more_threads` may add up to. A stopped
260 /// worker's place is not taken again. Below an explicit
261 /// [`worker_threads`](Self::worker_threads), [`build`](Self::build) fails
262 /// with `InvalidInput`; with `worker_threads` unset, the runtime starts
263 /// with at most this many.
264 pub fn max_worker_threads(mut self, n: usize) -> Self {
265 self.max_worker_threads = Some(n);
266 self
267 }
268
269 /// The most bytes each worker's IO buffers hold (default 64 MiB). Reads
270 /// that had to wait land in these buffers, and writes that had to wait
271 /// are copied into them; at the cap, IO waits for buffers to come back,
272 /// so a worker's IO memory never grows past it. On io_uring the worker's
273 /// receive buffers count against the cap too
274 /// ([`recv_buffers`](crate::config::IoUringConfiguration::recv_buffers) of
275 /// [`recv_buffer_size`](crate::config::IoUringConfiguration::recv_buffer_size)
276 /// bytes, 4 MiB by default, with their descriptors), and the rest is
277 /// left for the others. A cap that leaves less than 512 bytes beside the
278 /// receive buffers is refused with `InvalidInput` when the worker starts;
279 /// otherwise any value is accepted, and a request that cannot fit under a
280 /// cap too small for it fails with an error that names this setting.
281 pub fn io_buffer_cap(mut self, bytes: usize) -> Self {
282 self.io_buffer_cap = Some(bytes);
283 self
284 }
285
286 /// The kernel interface the workers run their IO on. Unset, it is the
287 /// process's: the `backend` of
288 /// [`NucleiConfig`](crate::config::NucleiConfig), or io_uring where the
289 /// kernel lets this process have it and the platform's poller otherwise.
290 /// A backend this process cannot have makes [`build`](Self::build) fail
291 /// with the reason. A non-empty `NUCLEI_BACKEND` overrides it.
292 pub fn backend(mut self, backend: crate::IoBackend) -> Self {
293 self.backend = Some(backend);
294 self
295 }
296
297 /// The settings the workers' io_uring rings are made with, for this
298 /// runtime alone (default: the process's, from
299 /// [`Proactor::with_config`](crate::Proactor::with_config)). Checked in
300 /// [`build`](Self::build), which fails with `InvalidInput` naming the
301 /// setting a ring cannot work with, on every platform; only the workers
302 /// of a runtime whose IO runs on io_uring make rings. Whether io_uring
303 /// is chosen when no backend is named is decided with these settings.
304 /// The shard of a thread inside `block_on` keeps the process's settings:
305 /// it serves every runtime the thread enters.
306 pub fn io_uring(mut self, config: crate::config::IoUringConfiguration) -> Self {
307 self.io_uring = Some(config);
308 self
309 }
310
311 /// Whether the runtime's clock starts paused (default false): see
312 /// [`time::pause`](crate::time::pause). Its tasks then see time move only
313 /// as they [`advance`](crate::time::advance) it, or as it jumps to the
314 /// next timer whenever every shard of the runtime is idle.
315 pub fn start_paused(mut self, paused: bool) -> Self {
316 self.start_paused = paused;
317 self
318 }
319
320 /// How long one poll of a task may run on a worker before the worker's
321 /// other tasks go on without it (default 10 ms; `None`: never).
322 ///
323 /// A task that blocks inside its poll (a blocking call, a long loop
324 /// without an `.await`) then holds up only itself: its worker's shard,
325 /// with every other task, timer and IO of it, goes to another thread on
326 /// the same core, and the blocked task carries on once its poll returns.
327 /// At least 1 ms; [`build`](Self::build) fails with `InvalidInput` below.
328 /// Tasks that `spawn_local` keeps on the blocked thread wait for it. The
329 /// watch costs each poll two plain stores and a load, and each call a
330 /// task makes into its worker (a wake, a spawn, a send or a receive) two
331 /// loads and two stores; with it on, an io_uring worker's ring takes
332 /// submissions from any thread.
333 pub fn handoff_after(mut self, after: Option<Duration>) -> Self {
334 self.handoff_after = Some(after);
335 self
336 }
337
338 /// Whether each worker thread is pinned to its own core (Linux and
339 /// Windows; default true). A pinned shard keeps its cache warm; unpinned,
340 /// the scheduler may move it. With
341 /// [`worker_cores`](Self::worker_cores) set, each worker is pinned to its
342 /// core of that list, or, unpinned, runs on any core of the list.
343 pub fn pin_workers(mut self, pin: bool) -> Self {
344 self.pin_workers = Some(pin);
345 self
346 }
347
348 /// Counts how long each task poll runs, and (with the `io-stats` feature)
349 /// how long each IO operation waits for the kernel, by kind, in log2
350 /// histograms each worker's [`ShardStats`](crate::stats::ShardStats)
351 /// carries (default false: no clock is read for it). The poll times cost
352 /// two clock reads and a call around every poll, of every runtime of the
353 /// process while one records them; the IO latencies two clock reads per
354 /// operation of this runtime's workers.
355 pub fn record_latencies(mut self, record: bool) -> Self {
356 self.record_latencies = record;
357 self
358 }
359
360 /// Counts each worker's IO operations (reads, writes, the bytes of each,
361 /// accepts) in its [`ShardStats::io`](crate::stats::ShardStats::io)
362 /// (default false: `io` is `None`). With the `io-stats` feature only,
363 /// which costs every IO call a few instructions, counted or not: off, a
364 /// count is one load of a flag per operation; on, a few plain
365 /// thread-local adds per operation and a drain each time a shard
366 /// publishes its counts, on every thread of the process while one
367 /// runtime counts.
368 #[cfg(feature = "io-stats")]
369 pub fn count_io(mut self, count: bool) -> Self {
370 self.count_io = count;
371 self
372 }
373
374 /// Starts the runtime.
375 ///
376 /// # Errors
377 ///
378 /// `InvalidInput` naming a setting that no runtime can work with, or
379 /// that does not fit with another; the reason when the backend cannot
380 /// be had, a worker's poller cannot be made or its thread cannot start.
381 pub fn build(self) -> io::Result<Runtime> {
382 self.validate()?;
383 let workers = self.worker_count();
384 let stack = self.thread_stack_size.unwrap_or(DEFAULT_STACK);
385 let limits = Limits {
386 max_nesting: self.max_nesting.unwrap_or(context::DEFAULT_MAX_NESTING),
387 buffer_cap: self.io_buffer_cap.unwrap_or(crate::buf::DEFAULT_CAP),
388 backend: super::super::backend::for_runtime(self.backend, self.io_uring.as_ref())?,
389 };
390 #[cfg(ring)]
391 let rings = limits.backend.rings();
392 let blocking_name = self
393 .thread_name
394 .clone()
395 .unwrap_or_else(|| Arc::new(|| "nuclei-blocking".to_string()));
396 let worker_name = self
397 .thread_name
398 .unwrap_or_else(|| Arc::new(|| "nuclei-worker".to_string()));
399 let pool = Pool::new(PoolConfig {
400 hooks: self.tuning.hooks.clone(),
401 max_threads: self
402 .max_blocking_threads
403 .unwrap_or(DEFAULT_BLOCKING_THREADS)
404 .max(1),
405 keep_alive: self.blocking_keep_alive.unwrap_or(DEFAULT_KEEP_ALIVE),
406 stack_size: self.thread_stack_size,
407 name: blocking_name,
408 cpu_mask: match &self.blocking_cores {
409 Some(cores) => super::cores::mask_of(cores),
410 None => super::current_mask(),
411 },
412 });
413 static IDS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
414 let (exit_tx, exits) = kovan_channel::unbounded();
415 let pin = self.pin_workers.unwrap_or(true);
416 let (cores, mask) = match &self.worker_cores {
417 Some(list) if pin => (list.clone(), None),
418 Some(list) => (Vec::new(), super::cores::mask_of(list)),
419 None if pin => (super::allowed_cores(), None),
420 None => (Vec::new(), None),
421 };
422 let capacity = self
423 .max_worker_threads
424 .unwrap_or(workers.saturating_mul(4))
425 .max(workers);
426 let shutdown_on_panic = self.unhandled_panic == UnhandledPanic::ShutdownRuntime;
427 let mut task_hooks = self.task_hooks;
428 super::super::notes::compose(
429 &mut task_hooks,
430 self.long_poll.is_some(),
431 self.record_latencies,
432 );
433 let inner = Arc::new_cyclic(|this| {
434 if shutdown_on_panic {
435 task_hooks.shutdown = Some(this.clone());
436 }
437 Inner {
438 id: IDS.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
439 workers: Workers::new(
440 capacity,
441 workers,
442 Start {
443 limits,
444 tuning: super::super::tuning::Tuning {
445 blocking: Some(pool.jobs_counter()),
446 record_latencies: self.record_latencies,
447 #[cfg(feature = "io-stats")]
448 count_io: self.count_io,
449 ..self.tuning.clone()
450 },
451 seed: self.rng_seed,
452 stack,
453 name: worker_name,
454 cores,
455 mask,
456 handoff_after: self.handoff_after.unwrap_or(Some(DEFAULT_HANDOFF)),
457 long_poll: self.long_poll,
458 },
459 exit_tx,
460 ),
461 pool,
462 local_margin: self.local_spawn_margin.unwrap_or(DEFAULT_LOCAL_MARGIN),
463 #[cfg(ring)]
464 rings,
465 clock: crate::time::clock::Clock::new(self.start_paused),
466 task_hooks: task_hooks.any().then(|| {
467 super::super::tuning::ANY_TASK_HOOKS
468 .store(true, std::sync::atomic::Ordering::Release);
469 Arc::new(task_hooks)
470 }),
471 shutdown_on_panic,
472 panicked: std::sync::atomic::AtomicBool::new(false),
473 on_panic: crate::sync::Notify::new(),
474 rng_seed: self.rng_seed,
475 guests: std::sync::OnceLock::new(),
476 lingering: super::super::linger::Lingering::new(),
477 io: self.io.resolve(),
478 }
479 });
480 inner.pool.set_owner(Arc::downgrade(&inner));
481 let runtime = Runtime { inner, exits };
482 // On an error the runtime drops: the workers already started stop.
483 Workers::start(&runtime.inner, workers)?;
484 Ok(runtime)
485 }
486
487 /// Starts the runtime and makes it the process default, the one the free
488 /// functions ([`spawn`](crate::spawn), [`block_on`](crate::block_on),
489 /// ...) use outside any other runtime.
490 ///
491 /// # Errors
492 ///
493 /// As for [`build`](Self::build), and `AlreadyExists` when the default
494 /// runtime is already running (it was built, or a free function started
495 /// it).
496 pub fn build_global(self) -> io::Result<RuntimeHandle> {
497 if DEFAULT.get().is_some() {
498 return Err(already_running());
499 }
500 let runtime = self.build()?;
501 let handle = runtime.handle();
502 DEFAULT.set(runtime).map_err(|_| already_running())?;
503 Ok(handle)
504 }
505}
506
507/// The error of a second default runtime.
508fn already_running() -> io::Error {
509 io::Error::new(
510 io::ErrorKind::AlreadyExists,
511 "the default runtime is already running",
512 )
513}
514
515impl fmt::Debug for Builder {
516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517 let mut builder = f.debug_struct("Builder");
518 builder
519 .field("worker_threads", &self.worker_threads)
520 .field("thread_stack_size", &self.thread_stack_size)
521 .field("max_nesting", &self.max_nesting)
522 .field("max_blocking_threads", &self.max_blocking_threads)
523 .field("thread_keep_alive", &self.blocking_keep_alive)
524 .field("pin_workers", &self.pin_workers)
525 .field("worker_cores", &self.worker_cores)
526 .field("blocking_cores", &self.blocking_cores)
527 .field("local_spawn_margin", &self.local_spawn_margin)
528 .field("io_buffer_cap", &self.io_buffer_cap)
529 .field("io_uring", &self.io_uring)
530 .field("start_paused", &self.start_paused)
531 .field("record_latencies", &self.record_latencies);
532 #[cfg(feature = "io-stats")]
533 builder.field("count_io", &self.count_io);
534 builder.finish_non_exhaustive()
535 }
536}