Skip to main content

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}