Skip to main content

simoom_rt/
runtime.rs

1//! Starting, observing and stopping a runtime.
2
3// Without the `udp` feature the UDP slots stay empty and their code is left out.
4#![cfg_attr(not(feature = "udp"), allow(dead_code, unused_imports, unused_mut))]
5
6use crate::event_loop::{build, ListenerSlot, LoopHandle, LoopSetup, UdpSlot};
7use crate::handler::{Acceptor, UdpService};
8use crate::offload::{OffloadConfig, OffloadPool};
9use crate::pool::{PoolStats, DEFAULT_CLASSES};
10use simoom_sys::driver::{Backend, DriverConfig, DriverStats};
11use simoom_sys::net::{ListenOptions, Socket, UdpOptions};
12use std::io;
13use std::net::{SocketAddr, ToSocketAddrs};
14use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
15use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
16use std::thread::JoinHandle;
17use std::time::Duration;
18
19/// Runtime settings. `Config::default()` suits a server.
20#[derive(Clone, Debug)]
21pub struct Config {
22    /// Loop threads; 0 means one per CPU this process may use.
23    pub threads: usize,
24    /// Pin loop `i` to CPU `cpus[i % cpus.len()]` (Linux and Windows).
25    pub pin_threads: bool,
26    /// The CPUs to pin to; `None` means the process affinity set.
27    pub cpus: Option<Vec<usize>>,
28    /// I/O driver settings, including the back end.
29    pub driver: DriverConfig,
30    /// Buffer class sizes; the smallest is also the receive buffer size.
31    pub buffer_classes: [usize; 3],
32    /// Most buffers each loop keeps cached in its smallest class.
33    pub pool_cache: usize,
34    /// Receive buffers each loop keeps lent to its driver (io_uring: in the provided-buffer ring).
35    pub recv_buffers: usize,
36    /// Most unconsumed input bytes per connection; beyond it the connection is closed.
37    pub max_input: usize,
38    /// Most connections in total; more are accepted and closed at once.
39    pub max_connections: usize,
40    /// Most connections per loop.
41    pub max_connections_per_loop: usize,
42    /// Above this many queued output bytes a connection stops reading.
43    pub write_high_water: usize,
44    /// At or below this many queued output bytes reading resumes.
45    pub write_low_water: usize,
46    /// Timer resolution.
47    pub timer_tick: Duration,
48    /// The shared offload pool.
49    pub offload: OffloadConfig,
50    /// Listener options (backlog, buffer sizes; `reuse_port` is set by the runtime). The
51    /// default backlog is the system maximum, and `defer_accept` is `Some(1)`: on Linux a
52    /// connection is handed over once its first bytes arrived, so the first request is read
53    /// in the same turn as the accept. Set `defer_accept` to `None` for a protocol where the
54    /// server speaks first.
55    pub listen: ListenOptions,
56    /// Loop thread name prefix.
57    pub thread_name: String,
58    /// Start only loop 0 at [`Builder::start`], without waiting for it (the listeners are bound
59    /// first, so connections wait in the backlog), and the other loops in the background when
60    /// loop 0 holds three connections at once. A server that is idle, or serves one connection
61    /// at a time (a FaaS instance), then pays for one loop thread in start-up time and memory
62    /// instead of one per core. Connections accepted while the other loops start (well under a
63    /// millisecond) are held, then spread over every loop, and loop 0 hands out its accepts for
64    /// a second after. A loop that fails to build its driver is logged on standard error
65    /// instead of failing `start` (binding errors still fail it). Ignored when the runtime has
66    /// UDP sockets ([`Builder::udp`]): every loop starts at once then, since a socket added
67    /// later to a `SO_REUSEPORT` group would take over some established QUIC connections.
68    /// Default `false` here (a load generator wants every loop at once); the `simoom` server
69    /// turns it on.
70    pub lazy_loops: bool,
71    /// Lingering close: after a graceful close ([`crate::Ctx::close`]) the write side is shut
72    /// down and input is read and dropped until the peer closes, for at most this long, before
73    /// the socket is closed (closing over unread input resets the connection, and a reset can
74    /// destroy the last response at the peer). `Duration::ZERO` closes at once. Default 2 s.
75    pub linger: Duration,
76    /// The most input a lingering close reads and drops before it closes anyway. Default
77    /// 256 KiB.
78    pub linger_bytes: usize,
79    /// Linux: descriptor table slots reserved at start, before the loop threads exist (capped
80    /// at the soft `RLIMIT_NOFILE`). Growing the table once threads run stalls the thread that
81    /// crosses each step (64, 128, 256...) for an RCU grace period, 10-30 ms on a VM. 0 skips
82    /// it. Default 1024 (8 KiB of kernel memory).
83    pub reserve_fds: usize,
84    /// Stack size of the loop threads; `None` keeps std's default (2 MiB, or `RUST_MIN_STACK`).
85    /// Only touched pages cost memory, so this mostly matters for address space.
86    pub stack_size: Option<usize>,
87    /// With the default loop count (`threads` 0, no `cpus`) on 4 CPUs or more: one loop fewer
88    /// than CPUs when the network's receive processing concentrates on one CPU (a NIC with a
89    /// single receive queue and no RPS, [`simoom_sys::cpu::net_rx_concentrated`]), and the loop
90    /// threads keep off that CPU. It spends most of its time in softirq at high packet rates, and
91    /// a loop sharing it falls behind the others with every connection it holds (K12: S1 at 256
92    /// connections on a 4-vCPU droplet, 111k req/s with four loops against 151k with three).
93    /// Leaving the CPU free is not enough: the scheduler moves a loop thread woken by that CPU's
94    /// softirq onto it, so unrestricted loops ran there 40% of the time. Default `true`.
95    pub leave_cpu_for_network: bool,
96}
97
98impl Default for Config {
99    fn default() -> Self {
100        Config {
101            threads: 0,
102            pin_threads: false,
103            cpus: None,
104            driver: DriverConfig::default(),
105            buffer_classes: DEFAULT_CLASSES,
106            pool_cache: 1024,
107            recv_buffers: 64,
108            max_input: 64 * 1024,
109            max_connections: 1 << 20,
110            max_connections_per_loop: 1 << 20,
111            write_high_water: 256 * 1024,
112            write_low_water: 64 * 1024,
113            timer_tick: Duration::from_millis(1),
114            offload: OffloadConfig::default(),
115            listen: ListenOptions { defer_accept: Some(1), ..ListenOptions::default() },
116            thread_name: "simoom-loop".into(),
117            stack_size: None,
118            reserve_fds: 1024,
119            linger: Duration::from_secs(2),
120            linger_bytes: 256 * 1024,
121            lazy_loops: false,
122            leave_cpu_for_network: true,
123        }
124    }
125}
126
127#[derive(Default)]
128pub(crate) struct LoopStats {
129    waits: AtomicU64,
130    recv_calls: AtomicU64,
131    send_calls: AtomicU64,
132    accept_calls: AtomicU64,
133    other_calls: AtomicU64,
134    sqes: AtomicU64,
135    os_events: AtomicU64,
136    sleeps: AtomicU64,
137    spin_polls: AtomicU64,
138    accepted: AtomicU64,
139    rejected: AtomicU64,
140    closed: AtomicU64,
141    active: AtomicU64,
142    pool_allocated: AtomicU64,
143    pool_reused: AtomicU64,
144    pool_cached: AtomicU64,
145    pub(crate) timers: AtomicU64,
146}
147
148impl LoopStats {
149    pub(crate) fn store(&self, d: DriverStats, p: PoolStats, accepted: u64, rejected: u64, closed: u64, active: u64) {
150        let r = Ordering::Relaxed;
151        self.waits.store(d.waits, r);
152        self.recv_calls.store(d.recv_calls, r);
153        self.send_calls.store(d.send_calls, r);
154        self.accept_calls.store(d.accept_calls, r);
155        self.other_calls.store(d.other_calls, r);
156        self.sqes.store(d.sqes, r);
157        self.os_events.store(d.os_events, r);
158        self.sleeps.store(d.sleeps, r);
159        self.spin_polls.store(d.spin_polls, r);
160        self.accepted.store(accepted, r);
161        self.rejected.store(rejected, r);
162        self.closed.store(closed, r);
163        self.active.store(active, r);
164        self.pool_allocated.store(p.allocated, r);
165        self.pool_reused.store(p.reused, r);
166        self.pool_cached.store(p.cached as u64, r);
167    }
168
169    fn load(&self) -> LoopSnapshot {
170        let r = Ordering::Relaxed;
171        LoopSnapshot {
172            driver: DriverStats {
173                waits: self.waits.load(r),
174                recv_calls: self.recv_calls.load(r),
175                send_calls: self.send_calls.load(r),
176                accept_calls: self.accept_calls.load(r),
177                other_calls: self.other_calls.load(r),
178                sqes: self.sqes.load(r),
179                os_events: self.os_events.load(r),
180                sleeps: self.sleeps.load(r),
181                spin_polls: self.spin_polls.load(r),
182            },
183            accepted: self.accepted.load(r),
184            rejected: self.rejected.load(r),
185            closed: self.closed.load(r),
186            active: self.active.load(r),
187            pool_allocated: self.pool_allocated.load(r),
188            pool_reused: self.pool_reused.load(r),
189            pool_cached: self.pool_cached.load(r),
190            timers: self.timers.load(r),
191        }
192    }
193}
194
195type Grow = Box<dyn FnOnce() + Send>;
196
197pub(crate) struct Shared {
198    pub(crate) stop: AtomicBool,
199    pub(crate) grace_ns: AtomicU64,
200    pub(crate) active: AtomicUsize,
201    pub(crate) loop_active: Vec<AtomicUsize>,
202    pub(crate) loops: Vec<LoopStats>,
203    /// The loops that run, by index.
204    handles: Mutex<Vec<LoopHandle>>,
205    /// `handles.len()`, read without the lock.
206    pub(crate) started: AtomicUsize,
207    /// Lazy loops that failed to start.
208    failed: AtomicUsize,
209    /// Every thread to join: the loops, and the one that starts lazy loops.
210    threads: Mutex<Vec<JoinHandle<()>>>,
211    /// Starts the loops not started yet ([`Config::lazy_loops`]); taken once.
212    grow: Mutex<Option<Grow>>,
213    /// The lazy loops are being started (connections accepted meanwhile are parked).
214    pub(crate) growing: AtomicBool,
215    /// Lazy start: loop 0's back end once it has built its driver (or failed to).
216    boot: Mutex<Option<(Backend, Option<String>)>>,
217    /// The first loop that failed to start: why.
218    error: Mutex<Option<io::Error>>,
219    boot_cv: Condvar,
220}
221
222// Out of line: the poison handling, inlined at each of its many call sites, costs about 2 KB.
223#[inline(never)]
224fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
225    m.lock().unwrap_or_else(PoisonError::into_inner)
226}
227
228impl Shared {
229    /// A loop runs: it can take tasks and handed-off connections. A loop that registers after
230    /// a stop began is woken, so it sees the stop at once.
231    pub(crate) fn register(&self, h: LoopHandle) {
232        let mut v = lock(&self.handles);
233        // kept sorted by index (an insertion: a sort call costs about 6 KB of code)
234        let at = v.iter().position(|l| l.index > h.index).unwrap_or(v.len());
235        v.insert(at, h.clone());
236        self.started.store(v.len(), Ordering::Release);
237        if self.stop.load(Ordering::Acquire) {
238            h.queue.waker.wake();
239        }
240        drop(v);
241        // for the thread waiting until the lazy loops are up
242        drop(lock(&self.boot));
243        self.boot_cv.notify_all();
244    }
245
246    /// The running loops, by index.
247    pub(crate) fn handles(&self) -> Vec<LoopHandle> {
248        lock(&self.handles).clone()
249    }
250
251    fn booted(&self, b: (Backend, Option<String>)) {
252        *lock(&self.boot) = Some(b);
253        self.boot_cv.notify_all();
254    }
255
256    /// Lazy start: waits until loop 0 has built its driver.
257    fn boot(&self) -> (Backend, Option<String>) {
258        let mut g = lock(&self.boot);
259        loop {
260            if let Some(b) = &*g {
261                return b.clone();
262            }
263            g = self.boot_cv.wait(g).unwrap_or_else(PoisonError::into_inner);
264        }
265    }
266
267    /// Starts the remaining loops if they were held back (first call only).
268    pub(crate) fn grow(&self) {
269        let f = lock(&self.grow).take();
270        if let Some(f) = f {
271            self.growing.store(true, Ordering::Release);
272            f();
273        }
274    }
275
276    /// The lazy loops are up (or failed): wake the loops so they spread parked connections.
277    fn grown(&self) {
278        self.growing.store(false, Ordering::Release);
279        for h in lock(&self.handles).iter() {
280            h.queue.waker.wake();
281        }
282    }
283}
284
285/// Counters of one loop, as last published by it (once per loop turn).
286#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
287pub struct LoopSnapshot {
288    /// The loop's driver counters (system calls and OS events).
289    pub driver: DriverStats,
290    /// Connections accepted (and kept).
291    pub accepted: u64,
292    /// Connections refused: over a cap, or the acceptor said no.
293    pub rejected: u64,
294    /// Connections closed.
295    pub closed: u64,
296    /// Connections open now.
297    pub active: u64,
298    /// Buffers newly allocated by the loop's pool.
299    pub pool_allocated: u64,
300    /// Buffers reused by the loop's pool.
301    pub pool_reused: u64,
302    /// Buffers cached in the loop's pool now.
303    pub pool_cached: u64,
304    /// Timers scheduled on the loop now (connections' and endpoints').
305    pub timers: u64,
306}
307
308/// The loop count for `cpus` CPUs when the configuration leaves it to the runtime (see
309/// [`Config::leave_cpu_for_network`]).
310fn default_loops(cfg: &Config, cpus: usize) -> usize {
311    loops_for(cfg.leave_cpu_for_network, cpus, simoom_sys::cpu::net_rx_concentrated)
312}
313
314/// [`default_loops`] with the receive-path check as a function (read only when it matters).
315fn loops_for(leave: bool, cpus: usize, concentrated: impl FnOnce() -> bool) -> usize {
316    if leave && cpus >= 4 && concentrated() {
317        cpus - 1
318    } else {
319        cpus
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::loops_for;
326
327    #[test]
328    fn a_cpu_is_left_to_a_concentrated_receive_path_from_four_cpus() {
329        assert_eq!(loops_for(true, 4, || true), 3);
330        assert_eq!(loops_for(true, 16, || true), 15);
331        assert_eq!(loops_for(true, 4, || false), 4, "receive work spread: every CPU");
332        assert_eq!(loops_for(true, 3, || panic!("not read below 4 CPUs")), 3);
333        assert_eq!(loops_for(false, 8, || panic!("not read when turned off")), 8);
334    }
335}
336
337/// Runtime counters.
338#[derive(Clone, Debug, Default)]
339pub struct Stats {
340    /// Per loop.
341    pub loops: Vec<LoopSnapshot>,
342    /// Connections open now, across loops.
343    pub active: usize,
344}
345
346impl Stats {
347    /// The per-loop counters summed.
348    pub fn total(&self) -> LoopSnapshot {
349        let mut t = LoopSnapshot::default();
350        for l in &self.loops {
351            t.driver.waits += l.driver.waits;
352            t.driver.recv_calls += l.driver.recv_calls;
353            t.driver.send_calls += l.driver.send_calls;
354            t.driver.accept_calls += l.driver.accept_calls;
355            t.driver.other_calls += l.driver.other_calls;
356            t.driver.sqes += l.driver.sqes;
357            t.driver.os_events += l.driver.os_events;
358            t.driver.sleeps += l.driver.sleeps;
359            t.driver.spin_polls += l.driver.spin_polls;
360            t.accepted += l.accepted;
361            t.rejected += l.rejected;
362            t.closed += l.closed;
363            t.active += l.active;
364            t.pool_allocated += l.pool_allocated;
365            t.pool_reused += l.pool_reused;
366            t.pool_cached += l.pool_cached;
367            t.timers += l.timers;
368        }
369        t
370    }
371}
372
373enum ListenSource {
374    Addr(io::Result<Vec<SocketAddr>>),
375    Socket(Socket),
376}
377
378/// Builds and starts a runtime. See [`crate::Runtime::builder`].
379pub struct Builder {
380    cfg: Config,
381    listeners: Vec<(ListenSource, Arc<dyn Acceptor>)>,
382    udps: Vec<(io::Result<Vec<SocketAddr>>, UdpOptions, Arc<dyn UdpService>)>,
383}
384
385impl std::fmt::Debug for Builder {
386    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387        f.debug_struct("Builder").field("cfg", &self.cfg).field("listeners", &self.listeners.len()).field("udps", &self.udps.len()).finish()
388    }
389}
390
391impl Default for Builder {
392    fn default() -> Self {
393        Builder::new()
394    }
395}
396
397impl Builder {
398    /// A builder with the default [`Config`].
399    pub fn new() -> Builder {
400        Builder { cfg: Config::default(), listeners: Vec::new(), udps: Vec::new() }
401    }
402
403    /// Replaces the whole configuration.
404    pub fn config(mut self, cfg: Config) -> Builder {
405        self.cfg = cfg;
406        self
407    }
408
409    /// Changes the configuration in place.
410    pub fn configure(mut self, f: impl FnOnce(&mut Config)) -> Builder {
411        f(&mut self.cfg);
412        self
413    }
414
415    /// Loop threads (0 = one per CPU).
416    pub fn threads(mut self, n: usize) -> Builder {
417        self.cfg.threads = n;
418        self
419    }
420
421    /// The I/O back end.
422    pub fn backend(mut self, b: Backend) -> Builder {
423        self.cfg.driver.backend = b;
424        self
425    }
426
427    /// Accepts TCP connections on `addr` and runs them with `acceptor`. On Linux each loop gets
428    /// its own `SO_REUSEPORT` listener; elsewhere one listener is shared and connections are
429    /// handed to the less busy of two loops (on Windows, through one `AcceptEx` queue).
430    /// Address errors are reported by [`Builder::start`].
431    pub fn listen(self, addr: impl ToSocketAddrs, acceptor: impl Acceptor) -> Builder {
432        let addrs = addr.to_socket_addrs().map(|a| a.collect());
433        self.listen_addrs(addrs, Arc::new(acceptor))
434    }
435
436    fn listen_addrs(mut self, addrs: io::Result<Vec<SocketAddr>>, acceptor: Arc<dyn Acceptor>) -> Builder {
437        // IPv4 first, as `localhost` should mean 127.0.0.1 (a stable partition, not a sort).
438        let addrs = addrs.map(|v| {
439            let (mut v4, v6): (Vec<SocketAddr>, Vec<SocketAddr>) = v.into_iter().partition(SocketAddr::is_ipv4);
440            v4.extend(v6);
441            v4
442        });
443        self.listeners.push((ListenSource::Addr(addrs), acceptor));
444        self
445    }
446
447    /// Accepts on an existing listener (socket activation, a shared port). It is shared by all
448    /// loops.
449    pub fn listen_on(mut self, listener: std::net::TcpListener, acceptor: impl Acceptor) -> Builder {
450        let _ = listener.set_nonblocking(true);
451        self.listeners.push((ListenSource::Socket(Socket::from(listener)), Arc::new(acceptor)));
452        self
453    }
454
455    /// Binds UDP on `addr` and creates an endpoint per loop with `service` (one `SO_REUSEPORT`
456    /// socket per loop on Linux; one socket on loop 0 elsewhere). Feature `udp`.
457    #[cfg(feature = "udp")]
458    pub fn udp(mut self, addr: impl ToSocketAddrs, opts: UdpOptions, service: impl UdpService) -> Builder {
459        let addrs = addr.to_socket_addrs().map(|a| a.collect());
460        self.udps.push((addrs, opts, Arc::new(service)));
461        self
462    }
463
464    /// Binds every listener, starts the loops and returns once they all run.
465    pub fn start(self) -> io::Result<RuntimeHandle> {
466        let mut cfg = self.cfg;
467        // UDP sockets are not lazy: SO_REUSEPORT spreads datagrams by hash over the sockets
468        // bound, so adding one later would send an established QUIC connection's datagrams
469        // to a loop that does not know it.
470        cfg.lazy_loops &= self.udps.is_empty();
471        let cpus = cfg.cpus.clone().unwrap_or_else(simoom_sys::cpu::allowed_cpus);
472        // Lazy loops with the default count: the affinity mask now, and the cgroup CPU quota
473        // (a few file reads, about 40 us) when the other loops start.
474        let quota_later = cfg.lazy_loops && cfg.threads == 0 && cfg.cpus.is_none();
475        let n = match (cfg.threads, &cfg.cpus) {
476            (0, Some(_)) => cpus.len().max(1),
477            (0, None) if quota_later => cpus.len().max(1),
478            (0, None) => default_loops(&cfg, simoom_sys::cpu::cpu_count()),
479            (t, _) => t,
480        };
481        cfg.threads = n;
482        if cfg.write_low_water > cfg.write_high_water {
483            cfg.write_low_water = cfg.write_high_water;
484        }
485        let per_loop = cfg!(target_os = "linux");
486        // Grow the descriptor table now, while (usually) only this thread exists: growing it
487        // later stalls the loop thread that crosses a step for an RCU grace period (measured
488        // 18-28 ms on WSL2 at start with 12 loops and HTTP/3, and again at the first accepts).
489        simoom_sys::net::reserve_descriptors(cfg.reserve_fds);
490
491        // Listener sockets per loop.
492        let mut per_loop_listeners: Vec<Vec<ListenerSlot>> = (0..n).map(|_| Vec::new()).collect();
493        let mut addrs = Vec::new();
494        for (src, acceptor) in self.listeners {
495            match src {
496                ListenSource::Socket(s) => {
497                    addrs.push(s.local_addr()?);
498                    for slots in per_loop_listeners.iter_mut() {
499                        slots.push(ListenerSlot::new(acceptor.clone(), None, true));
500                    }
501                    per_loop_listeners[0].last_mut().unwrap().socket = Some(s);
502                }
503                ListenSource::Addr(a) => {
504                    let list = a?;
505                    let first = *list.first().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no address to listen on"))?;
506                    let mut opts = cfg.listen.clone();
507                    opts.reuse_port = per_loop;
508                    let s0 = Socket::tcp_listen(first, &opts)?;
509                    let bound = s0.local_addr()?;
510                    addrs.push(bound);
511                    // Loop 0 gets the socket bound here (it fixes the port); the other loops bind
512                    // their SO_REUSEPORT sockets on their own threads, in parallel.
513                    let mut s0 = Some(s0);
514                    for (i, slots) in per_loop_listeners.iter_mut().enumerate() {
515                        let mut slot = ListenerSlot::new(acceptor.clone(), s0.take(), !per_loop);
516                        if per_loop && i > 0 {
517                            slot.bind = Some((bound, opts.clone()));
518                        }
519                        slots.push(slot);
520                    }
521                }
522            }
523        }
524
525        // UDP sockets per loop.
526        let mut per_loop_udps: Vec<Vec<UdpSlot>> = (0..n).map(|_| Vec::new()).collect();
527        let mut udp_addrs = Vec::new();
528        #[cfg(feature = "udp")]
529        for (b, (a, opts, service)) in self.udps.into_iter().enumerate() {
530            let list = a?;
531            let first = *list.first().ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no address to bind"))?;
532            let mut opts = opts;
533            opts.reuse_port = per_loop;
534            let s0 = Socket::udp_bind(first, &opts)?;
535            let bound = s0.local_addr()?;
536            udp_addrs.push(bound);
537            per_loop_udps[0].push(UdpSlot::new(service.clone(), b, Some(s0), opts.gro));
538            if per_loop {
539                for slots in per_loop_udps.iter_mut().skip(1) {
540                    let mut slot = UdpSlot::new(service.clone(), b, None, opts.gro);
541                    slot.bind = Some((bound, opts.clone()));
542                    slots.push(slot);
543                }
544            }
545        }
546
547        let shared = Arc::new(Shared {
548            stop: AtomicBool::new(false),
549            grace_ns: AtomicU64::new(0),
550            active: AtomicUsize::new(0),
551            loop_active: (0..n).map(|_| AtomicUsize::new(0)).collect(),
552            loops: (0..n).map(|_| LoopStats::default()).collect(),
553            handles: Mutex::new(Vec::with_capacity(n)),
554            started: AtomicUsize::new(0),
555            failed: AtomicUsize::new(0),
556            threads: Mutex::new(Vec::with_capacity(n + 1)),
557            grow: Mutex::new(None),
558            growing: AtomicBool::new(false),
559            boot: Mutex::new(None),
560            error: Mutex::new(None),
561            boot_cv: Condvar::new(),
562        });
563        let mut ocfg = cfg.offload.clone();
564        if ocfg.name.is_empty() {
565            ocfg.name = "simoom-offload".into();
566        }
567        let offload = OffloadPool::new(ocfg);
568        let cfg = Arc::new(cfg);
569        let mut setups: Vec<LoopSetup> = per_loop_listeners
570            .into_iter()
571            .zip(per_loop_udps)
572            .enumerate()
573            .map(|(i, (listeners, udps))| LoopSetup { index: i, cfg: cfg.clone(), shared: shared.clone(), offload: offload.clone(), listeners, udps })
574            .collect();
575        let pin_of = {
576            let cpus = cpus.clone();
577            let pin = cfg.pin_threads;
578            move |i: usize| if pin && !cpus.is_empty() { Some(cpus[i % cpus.len()]) } else { None }
579        };
580        if cfg.lazy_loops {
581            // Loop 0 now, without waiting for it: its listeners are bound, so connections
582            // queue in the backlog until it runs (it reports its back end through `boot`). The
583            // other loops start when three connections are open at once.
584            let later = setups.split_off(1);
585            let setup = setups.pop().expect("loop 0");
586            let t = spawn_loop(&cfg, setup, pin_of(0), shared.clone())?;
587            lock(&shared.threads).push(t);
588            if !later.is_empty() {
589                let cfg = cfg.clone();
590                let weak = Arc::downgrade(&shared);
591                *lock(&shared.grow) = Some(Box::new(move || {
592                    // Called on the loop thread that saw the first connection: a helper creates
593                    // the threads, so that loop goes on serving.
594                    let w = weak.clone();
595                    let helper = crate::spawn(std::thread::Builder::new().name(format!("{}-start", cfg.thread_name)), Box::new(move || {
596                        let mut later = later;
597                        if quota_later {
598                            later.truncate(default_loops(&cfg, simoom_sys::cpu::cpu_count()).saturating_sub(1));
599                        }
600                        let mut spawned = 0;
601                        for setup in later {
602                            let Some(shared) = w.upgrade() else { return };
603                            if shared.stop.load(Ordering::Acquire) {
604                                break;
605                            }
606                            let i = setup.index;
607                            match spawn_loop(&cfg, setup, pin_of(i), shared.clone()) {
608                                Ok(t) => {
609                                    lock(&shared.threads).push(t);
610                                    spawned += 1;
611                                }
612                                Err(e) => log_failure(i, "failed to start", &e),
613                            }
614                        }
615                        // every loop registered (or failed): parked connections can be spread
616                        if let Some(shared) = w.upgrade() {
617                            let mut g = lock(&shared.boot);
618                            while shared.started.load(Ordering::Acquire) + shared.failed.load(Ordering::Acquire) < 1 + spawned {
619                                g = shared.boot_cv.wait(g).unwrap_or_else(PoisonError::into_inner);
620                            }
621                            drop(g);
622                            shared.grown();
623                        }
624                    }));
625                    match (helper, weak.upgrade()) {
626                        (Ok(t), Some(shared)) => lock(&shared.threads).push(t),
627                        (Err(e), Some(shared)) => {
628                            eprintln!("simoom: the loops could not be started: {e}");
629                            shared.grown();
630                        }
631                        _ => {}
632                    }
633                }));
634            }
635            return Ok(RuntimeHandle { shared, addrs, udp_addrs, offload, backend: OnceLock::new(), lazy: true });
636        }
637        let now = setups.len();
638
639        // Every loop at once (the same thread body as a lazy start); wait until each has built
640        // its driver and registered, or failed.
641        for setup in setups {
642            let i = setup.index;
643            match spawn_loop(&cfg, setup, pin_of(i), shared.clone()) {
644                Ok(t) => lock(&shared.threads).push(t),
645                Err(e) => {
646                    *lock(&shared.error) = Some(e);
647                    break;
648                }
649            }
650        }
651        let mut g = lock(&shared.boot);
652        let spawned = lock(&shared.threads).len();
653        while shared.started.load(Ordering::Acquire) + shared.failed.load(Ordering::Acquire) < spawned {
654            g = shared.boot_cv.wait(g).unwrap_or_else(PoisonError::into_inner);
655        }
656        drop(g);
657        let mut handle = RuntimeHandle { shared, addrs, udp_addrs, offload, backend: OnceLock::new(), lazy: false };
658        if spawned < now || handle.shared.failed.load(Ordering::Acquire) != 0 {
659            // A loop failed: stop the others (they serve nothing yet) and report its error.
660            handle.stop(Duration::ZERO);
661            handle.join_threads();
662            let e = lock(&handle.shared.error).take();
663            return Err(e.unwrap_or_else(|| io::Error::other("a loop thread died during start")));
664        }
665        Ok(handle)
666    }
667}
668
669/// Counts a loop thread as failed unless it registered (also when `build` panics), so a start
670/// waiting for the loops never hangs.
671struct Starting {
672    shared: Arc<Shared>,
673    done: bool,
674}
675
676impl Drop for Starting {
677    fn drop(&mut self) {
678        if !self.done {
679            self.shared.failed.fetch_add(1, Ordering::AcqRel);
680            drop(lock(&self.shared.boot));
681            self.shared.boot_cv.notify_all();
682        }
683    }
684}
685
686/// Starts a loop thread: it builds its driver and registers itself. A failure is counted in
687/// `failed` and kept in `error`; with a lazy start it is logged too (nothing waits for it).
688fn spawn_loop(cfg: &Config, setup: LoopSetup, pin: Option<usize>, shared: Arc<Shared>) -> io::Result<JoinHandle<()>> {
689    let i = setup.index;
690    let log = cfg.lazy_loops;
691    crate::spawn(loop_thread(cfg, i), Box::new(move || {
692        if let Some(cpu) = pin {
693            let _ = simoom_sys::cpu::pin_current_thread(cpu);
694        } else {
695            // off the CPU left to the network, if the loop count left one
696            simoom_sys::cpu::keep_current_thread_off_net_rx();
697        }
698        let mut starting = Starting { shared, done: false };
699        let shared = &starting.shared;
700        let (mut core, queue) = match build(setup) {
701            Ok(x) => x,
702            Err(e) => {
703                if log {
704                    log_failure(i, "failed to start", &e);
705                }
706                if i == 0 {
707                    shared.booted((Backend::Auto, Some(format!("loop 0 failed to start: {e}"))));
708                }
709                lock(&shared.error).get_or_insert(e);
710                return;
711            }
712        };
713        if i == 0 {
714            shared.booted(core.backend());
715        }
716        shared.register(LoopHandle { index: i, queue });
717        starting.done = true;
718        drop(starting);
719        if let Err(e) = core.run() {
720            log_failure(i, "failed", &e);
721        }
722    }))
723}
724
725/// Logs a loop's failure: one copy of the formatting for every site.
726#[cold]
727#[inline(never)]
728fn log_failure(i: usize, what: &str, e: &io::Error) {
729    eprintln!("simoom loop {i} {what}: {e}");
730}
731
732fn loop_thread(cfg: &Config, i: usize) -> std::thread::Builder {
733    let tb = std::thread::Builder::new().name(format!("{}-{i}", cfg.thread_name));
734    match cfg.stack_size {
735        Some(sz) => tb.stack_size(sz),
736        None => tb,
737    }
738}
739
740/// A running runtime. Dropping it stops the runtime at once (no grace) and waits for its
741/// threads; call [`RuntimeHandle::stop`] and [`RuntimeHandle::join`] for a graceful stop.
742pub struct RuntimeHandle {
743    shared: Arc<Shared>,
744    addrs: Vec<SocketAddr>,
745    udp_addrs: Vec<SocketAddr>,
746    offload: OffloadPool,
747    /// The back end and the fallback reason; with a lazy start, filled on first ask.
748    backend: OnceLock<(Backend, Option<String>)>,
749    lazy: bool,
750}
751
752impl std::fmt::Debug for RuntimeHandle {
753    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
754        f.debug_struct("RuntimeHandle").field("loops", &self.shared.started.load(Ordering::Relaxed)).field("addrs", &self.addrs).field("lazy", &self.lazy).finish()
755    }
756}
757
758impl RuntimeHandle {
759    /// The bound TCP addresses, in the order of [`Builder::listen`] calls.
760    pub fn local_addrs(&self) -> &[SocketAddr] {
761        &self.addrs
762    }
763
764    /// The bound UDP addresses, in the order of [`Builder::udp`] calls.
765    pub fn udp_addrs(&self) -> &[SocketAddr] {
766        &self.udp_addrs
767    }
768
769    /// The loops that run, by index, to run tasks on them. With [`Config::lazy_loops`] that is
770    /// loop 0 until three connections were open at once, then each loop as it starts.
771    pub fn loops(&self) -> Vec<LoopHandle> {
772        self.shared.handles()
773    }
774
775    fn boot(&self) -> &(Backend, Option<String>) {
776        self.backend.get_or_init(|| self.shared.boot())
777    }
778
779    /// The back end the loops use. With [`Config::lazy_loops`] it waits until loop 0 has
780    /// built its driver (well under a millisecond after [`Builder::start`]).
781    pub fn backend(&self) -> Backend {
782        self.boot().0
783    }
784
785    /// Why the configured back end was replaced (io_uring falling back to epoll), if it was.
786    pub fn fallback_reason(&self) -> Option<&str> {
787        self.boot().1.as_deref()
788    }
789
790    /// The shared offload pool.
791    pub fn offload(&self) -> &OffloadPool {
792        &self.offload
793    }
794
795    /// Current counters.
796    pub fn stats(&self) -> Stats {
797        Stats { loops: self.shared.loops.iter().map(LoopStats::load).collect(), active: self.shared.active.load(Ordering::Relaxed) }
798    }
799
800    /// Starts a graceful stop: listeners close, every connection gets
801    /// [`crate::Connection::on_stop`], and connections still open after `grace` are aborted.
802    /// Loops held back by [`Config::lazy_loops`] are not started any more.
803    pub fn stop(&self, grace: Duration) {
804        self.shared.grace_ns.store(grace.as_nanos().min(u64::MAX as u128) as u64, Ordering::Release);
805        self.shared.stop.store(true, Ordering::Release);
806        // closes the sockets of loops never started
807        drop(lock(&self.shared.grow).take());
808        for l in lock(&self.shared.handles).iter() {
809            l.queue.waker.wake();
810        }
811    }
812
813    /// Waits for every loop to exit (after [`RuntimeHandle::stop`]), then stops the offload
814    /// pool.
815    pub fn join(mut self) {
816        self.join_threads();
817    }
818
819    fn join_threads(&mut self) {
820        // The helper that starts lazy loops adds their threads while it runs: pop until empty.
821        loop {
822            let t = lock(&self.shared.threads).pop();
823            match t {
824                Some(t) => {
825                    let _ = t.join();
826                }
827                None => break,
828            }
829        }
830        self.offload.shutdown(Duration::from_secs(5));
831    }
832}
833
834impl Drop for RuntimeHandle {
835    fn drop(&mut self) {
836        let running = !lock(&self.shared.threads).is_empty();
837        if running {
838            if !self.shared.stop.load(Ordering::Acquire) {
839                self.stop(Duration::ZERO);
840            }
841            self.join_threads();
842        }
843    }
844}