//! Worker execution and runner-wide lifecycle. //! //! A [pool](super::pool) of workers polls ordinary tasks, worker zero on the //! calling thread and the others on threads of their own. Dedicated and //! blocking tasks each get a separate thread and io_uring ring. All workers //! share configuration and services through [`Shared`], while execution state //! stays in each worker's [`Local`]. //! //! ## State and ownership //! //! ```text //! Context //! +-- Shared (Arc) <-------------------------- Local.shared //! | +-- Config, metrics, stop/panic signals //! | +-- Network, Storage //! | +-- Pool (each pool worker's mailbox and run queue, global queue, //! | | idle state, shutdown barrier) //! | | ^-- task headers (Weak) //! | +-- Tasks (every live ordinary task, for teardown) //! | `-- Workers <------------------------ ActiveWorker //! `-- supervision tree //! //! Worker (one per thread) //! +-- Scope --------------------------------> CURRENT (TLS) //! | | //! +-- Rc> <------------------------+ //! | +-- Shared (Arc) //! | +-- Ready (owner's end of a pool worker's run queue) //! | +-- Timers (sleep registrations and deadlines) //! | +-- Driver //! | | +-- io_uring ring (SQ/CQ) //! | | +-- Waiters (requests and unconsumed results) //! | | `-- ready queue, deadlines, cancellations (IDs) //! | +-- Mailbox (messages and wake source) //! | `-- Deferred (callbacks and retired owners) //! +-- inbox (batch taken from Mailbox) //! +-- deferred (batch detached from Local) //! `-- panics (first failure retained through cleanup) //! ``` //! //! [`Scope`] installs checked thread-local access. Task polls, callbacks, and //! user destructors run outside local borrows. [`Worker::callbacks`] swaps the //! local [`Deferred`] batch into the worker before running it, so callbacks can //! reenter [`Local`] and append another batch. //! //! The root is pinned separately and passed to [`Worker::drive`]. On a //! dedicated worker, the dedicated or blocking task it was started for runs as //! its root. Pool workers after worker zero run a root that ends when worker //! zero stops the pool. //! //! ## Task placement //! //! Factories run on the spawning caller. Registration allocates an ordinary //! task and retains it in [`Tasks`] on the caller's thread, then delivers its //! first runnable. A pool worker keeps it when nothing else is queued there or //! when it is the pool's only worker. Every other spawn, including one from a //! busy pool worker, goes to the pool's global queue, which wakes a parked //! worker. //! //! A wake queues the task in the run queue of the pool worker that wakes it. A //! wake from outside the pool or on a closing worker pushes it into the global //! queue, which any pool worker takes from. A wake during a poll requeues the //! task on its poller. A queued task can still move, to the global queue when //! its run queue overflows or to a worker that steals it, as the //! [pool](super::pool) describes. //! //! A push that leaves a stealable backlog offers it to a parked worker unless //! an existing search count covers finding or handing on that work. A count //! can belong to a searching worker or to a wake that has reserved it but has //! not yet claimed a worker. //! //! ```text //! ordinary spawn: //! check Tasks --> factory --> Tasks::register //! +-- pool worker, quiet or alone --> its run queue //! `-- otherwise --> global queue --> any pool worker //! +-- first: poll //! `-- rest: run queue //! //! dedicated / blocking spawn: //! Workers::reserve --> factory --> new thread //! | //! v //! Worker::run_task //! | //! v //! drop ActiveWorker //! ``` //! //! [`ActiveWorker`] keeps the launch counted from before factory construction //! until worker cleanup and failure reporting finish. It retains only the //! [`Workers`] barrier, allowing that worker's [`Shared`] owners to be released first. //! //! I/O and sleeps register directly with the worker that first polls them. When //! one is polled later on another worker, the registering worker forwards its //! result through a channel. Mailboxes carry root wakes, observation transfers, //! and cancellation messages. //! //! Each turn polls a bounded batch of tasks and checks the root's wake flag, //! services I/O and timers, and applies a bounded batch of mailbox messages. //! Deferred callbacks run between these phases. //! //! A pool worker takes a share of the global queue every //! [`Config::with_global_queue_interval`] takes and whenever its own run queue //! is empty. Taking a share returns one runnable to poll next and queues the //! rest in the run queue, no more than it has room for and no more than would //! fill it to half. A pool worker that finds both queues empty after a service //! pass tries to steal before parking, or relies on workers already holding //! search counts when the search cap refuses it. //! //! Before parking, the worker checks readiness again because polls and //! callbacks can produce more work. Pool workers coordinate through //! [`Searcher`], and all workers wait through their mailbox's //! [`Waker`](super::waker::Waker). See [`super::idle`] for parking order. //! //! A held search count suppresses kicks, so a worker returns it before user //! code and before another turn without parking. Every drive-loop exit, //! including unwinding, withdraws any live idle publication and returns a held //! count before root destruction. //! //! ## Shutdown //! //! Once worker zero is running, runner exit follows this order: //! //! ```text //! root completes or fails //! | //! v //! destroy root (TLS and mailbox still available) //! | //! v //! close worker registry, task set, and global queue //! | //! v //! abort spawned tasks through the supervision tree, stop the other pool workers //! | //! v //! every pool worker discards its run queue and drains the task set //! | //! v //! every pool worker waits for the others to finish polling and draining //! | //! v //! pool workers close mailbox and io_uring ring --> join the pool workers //! | //! v //! collect a pool worker failure the root missed //! | //! v //! wait for dedicated workers to finish //! | //! v //! return result or resume panic //! ``` //! //! No pool worker closes its mailbox or io_uring ring until every pool worker //! has drained the set and finished its last poll, since a task polled on one //! worker can hold registrations on another, whose mailbox forwards their //! results. A dedicated worker closes its mailbox once its root is gone. //! //! Work spawned by root destruction participates in shutdown. Each worker keeps //! TLS installed while destroying tasks, draining retained writes and syncs, //! retiring cancellations, and running deferred callbacks. Native thread-local //! destruction on dedicated threads can follow registration release. use super::{ driver::Driver, idle::Searcher, mailbox::{Cancel, Forward, Mailbox, Message}, pool::{MAX_WORKERS, Pool, Threads}, request::{RequestOutput, RetiredResources}, sleep::{Sleep, Timers}, spinner::{Config as SpinnerConfig, Spinner}, task::{AfterPoll, Ready, RootWaker, Runnable}, tasks::Tasks, timeout::TimeoutWheel, waker::SUBMISSION_SEQ_MASK, }; #[cfg(feature = "external")] use crate::Pacer; use crate::{ BlobLayout, BlobVersion, Clock, Error, Execution, Handle, METRICS_PREFIX, Name, SinkOf, Spawner as _, StreamOf, Supervisor as _, child_label, network::{ iouring::{Config as NetworkConfig, Network}, metered::Network as MeteredNetwork, }, prefixed_name, process::metered::Metrics as ProcessMetrics, signal::Signal, storage::{ iouring::{Config as StorageConfig, Storage}, metered::Storage as MeteredStorage, }, telemetry::metrics::{ CounterFamily, Gauge, GaugeFamily, Metric, Register, Registered, Registry, add_attribute, raw, task::Label, validate_label, }, utils::{self, FactoryGuard, MetricHandle, Panicker, signal::Stopper, supervision::Tree}, }; use commonware_macros::select; use commonware_parallel::{Rayon, rayon::ThreadPoolBuilder}; use commonware_utils::{ channel::oneshot, sync::{Condvar, Mutex}, sys_rng, }; use governor::clock::{Clock as GClock, ReasonablyRealtime}; use rand_core::{Rng, TryCryptoRng, TryRng}; use std::{ any::Any, cell::RefCell, convert::Infallible, env, future::{Future, poll_fn}, mem, net::{IpAddr, SocketAddr, ToSocketAddrs}, num::{NonZeroU64, NonZeroUsize}, ops::RangeInclusive, panic::{AssertUnwindSafe, catch_unwind, resume_unwind}, path::PathBuf, pin::{Pin, pin}, ptr, rc::Rc, sync::{Arc, Weak}, task::{Context as TaskContext, Poll, Waker}, time::{Duration, Instant, SystemTime}, }; /// Maximum task polls or mailbox messages processed before yielding to other work. const BATCH_SIZE: usize = 64; /// Pinned future polled as a dedicated worker's root, or the runner's service /// task before it becomes a [`Task`](super::task::Task). type BoxedTask = Pin + Send>>; /// Configuration of each worker's io_uring instance and operation timing wheel. /// /// The runtime requires Linux 6.1 or newer and always uses single-issuer mode /// with deferred task work. The same configuration applies to pool and /// dedicated workers. The wheel horizon is derived from network timeout policy. #[derive(Clone, Debug)] pub struct RingConfig { /// SQ size and maximum outstanding operation SQEs, rounded up to a power of two. /// /// Must be nonzero and round to at most 32,768. Defaults to 128. The runtime /// chooses 1024 for its production default and 128 when built for tests. /// /// The CQ uses the kernel default of twice the rounded SQ size. Cancellation /// and mailbox wake CQEs also use that space, and cancellation acknowledgements /// can outlive their requests. The operation limit therefore does not bound /// all pending CQEs. The runtime relies on the kernel's CQ overflow backlog /// (`IORING_FEAT_NODROP`), drained by subsequent completion-service enters. pub size: u32, /// Nonzero operation deadline granularity, defaulting to 5 milliseconds. /// /// Smaller ticks improve precision but increase wheel storage and service /// frequency. Configuration must fit within 1,048,576 wheel slots. pub timeout_wheel_tick: Duration, } impl Default for RingConfig { fn default() -> Self { Self { size: 128, timeout_wheel_tick: Duration::from_millis(5), } } } /// Configuration for the native io_uring runtime. /// /// A pool of [`Self::with_worker_threads`] workers polls ordinary tasks, the /// first on the thread calling [`crate::Runner::start`]. Tasks marked dedicated /// or blocking each receive a fresh thread and io_uring ring. Shutdown /// completes retained writes and syncs without a configured time limit. #[derive(Clone)] pub struct Config { /// Pool workers, including the calling thread. worker_threads: usize, /// Every Nth poll of a busy pool worker looks at the global queue first, /// defaulting to 31. global_queue_interval: u32, /// Per-worker io_uring ring capacity and operation wheel tick. ring_config: RingConfig, /// Idle spinning policy, shared by all workers. idle_spinner: SpinnerConfig, /// Stack size for pool, dedicated worker, and Rayon threads. thread_stack_size: usize, /// Whether spawned-task panics and dedicated worker failures are caught. /// Task-disposal panics during worker execution are contained with either setting. catch_panics: bool, /// Base directory held while storage resources or requests remain alive. storage_directory: PathBuf, /// Accepted blob layouts, defaulting to the complete supported range. storage_blob_layouts: RangeInclusive, /// Optional TCP_NODELAY override, defaulting to Some(true). tcp_nodelay: Option, /// Request immediate reset on socket close, defaulting to true. zero_linger: bool, /// Whole-call outbound connection deadline, defaulting to 10 seconds. connect_timeout: Duration, /// Whole-call send and receive deadline, defaulting to 60 seconds. read_write_timeout: Duration, /// Network receive buffering capacity, defaulting to 64 KiB. read_buffer_size: usize, } impl Config { /// Return the default runtime configuration with a generated temporary directory. pub fn new() -> Self { let ring_config = RingConfig { size: if cfg!(test) { 128 } else { 1024 }, ..RingConfig::default() }; let suffix = sys_rng().next_u64(); Self { worker_threads: 2, global_queue_interval: 31, ring_config, idle_spinner: SpinnerConfig::default(), thread_stack_size: utils::thread::system_thread_stack_size(), catch_panics: false, storage_directory: env::temp_dir().join(format!("commonware_iouring_runtime_{suffix}")), storage_blob_layouts: BlobLayout::ALL, tcp_nodelay: Some(true), zero_linger: true, connect_timeout: Duration::from_secs(10), read_write_timeout: Duration::from_secs(60), read_buffer_size: 64 * 1024, } } /// Set the number of pool workers, including the calling thread. Defaults /// to 2. /// /// Must be between 1 and 64. Each worker has its own io_uring ring. /// Dedicated and blocking threads are additional to this count. pub const fn with_worker_threads(mut self, workers: usize) -> Self { self.worker_threads = workers; self } /// Set how often a busy pool worker looks at the global queue first: every /// `interval`th poll. Defaults to 31. /// /// Must be nonzero. A smaller interval starts tasks in the global queue /// sooner at the cost of more synchronization. At 1, a busy worker checks /// the global queue before every poll and polls its own queued tasks only /// while the global queue is empty. pub const fn with_global_queue_interval(mut self, interval: u32) -> Self { self.global_queue_interval = interval; self } /// Set io_uring ring capacity and deadline granularity. See [`RingConfig`]. pub const fn with_ring_config(mut self, config: RingConfig) -> Self { self.ring_config = config; self } /// Set idle spinning policy. Use [`SpinnerConfig::disabled`] to disable it. pub const fn with_idle_spinner(mut self, config: SpinnerConfig) -> Self { self.idle_spinner = config; self } /// Set the stack size of pool, dedicated worker, and Rayon threads. Worker /// zero runs on the calling thread and keeps its stack. pub const fn with_thread_stack_size(mut self, size: usize) -> Self { self.thread_stack_size = size; self } /// Set whether spawned-task panics and dedicated worker failures are caught. /// /// Caught failures are logged without interrupting the root. Propagated /// failures are observed only while the root is executing. A failure of a /// pool worker itself, such as an io_uring error, fails the runner with /// either setting. It interrupts the root while the root runs, and /// otherwise the runner resumes it after shutdown. /// /// A worker that fails to start, for example because its io_uring ring /// cannot be created, reports the error through the panic hook. With /// `panic = "abort"`, such a failure aborts the process with either /// setting, like a failure to spawn the worker's thread. /// /// During worker execution, cancellation-time and task-disposal panics /// escaping the user-poll wrapper are contained with either setting. An /// unpublished result may resolve to [`Error::Closed`], while an /// already-published result remains available. /// Task factories execute synchronously and propagate panics to their caller. pub const fn with_catch_panics(mut self, catch: bool) -> Self { self.catch_panics = catch; self } /// Set the outbound connection timeout, including queueing and retries. /// /// Must be nonzero and no greater than 30 years. The maximum configured /// network timeout must fit the wheel slot limit in [`RingConfig`]. pub const fn with_connect_timeout(mut self, timeout: Duration) -> Self { self.connect_timeout = timeout; self } /// Set the send and receive timeout, including queueing and partial progress. /// /// Must be nonzero and no greater than 30 years. The maximum configured /// network timeout must fit the wheel slot limit in [`RingConfig`]. pub const fn with_read_write_timeout(mut self, timeout: Duration) -> Self { self.read_write_timeout = timeout; self } /// Set the best-effort TCP_NODELAY override. None leaves the system default. pub const fn with_tcp_nodelay(mut self, enabled: Option) -> Self { self.tcp_nodelay = enabled; self } /// Set whether sockets request zero linger when configured. pub const fn with_zero_linger(mut self, enabled: bool) -> Self { self.zero_linger = enabled; self } /// Set the network receive buffer size in bytes. pub const fn with_read_buffer_size(mut self, size: usize) -> Self { self.read_buffer_size = size; self } /// Set the storage directory, created and held when the runner starts. pub fn with_storage_directory(mut self, directory: impl Into) -> Self { self.storage_directory = directory.into(); self } /// Set the blob layouts accepted by storage. /// /// New blobs use the latest layout in this range. Existing blobs outside /// it fail to open with [`crate::Error::BlobLayoutMismatch`]. Restrict the /// range to what a rollback target can read before first opening storage. /// /// # Panics /// /// Panics if `layouts` is empty. pub fn with_storage_blob_layouts(mut self, layouts: RangeInclusive) -> Self { assert!( !layouts.is_empty(), "storage blob layouts must be non-empty" ); self.storage_blob_layouts = layouts; self } /// Return the number of pool workers, including the calling thread. pub const fn worker_threads(&self) -> usize { self.worker_threads } /// Return how often a busy pool worker looks at the global queue first: /// every Nth poll. pub const fn global_queue_interval(&self) -> u32 { self.global_queue_interval } /// Return per-worker io_uring ring configuration. pub const fn ring_config(&self) -> &RingConfig { &self.ring_config } /// Return per-worker idle spinning configuration. pub const fn idle_spinner(&self) -> &SpinnerConfig { &self.idle_spinner } /// Return the configured pool, dedicated worker, and Rayon thread stack size. pub const fn thread_stack_size(&self) -> usize { self.thread_stack_size } /// Return whether spawned-task panics and dedicated worker failures are caught. pub const fn catch_panics(&self) -> bool { self.catch_panics } /// Return the whole-call outbound connection timeout. pub const fn connect_timeout(&self) -> Duration { self.connect_timeout } /// Return the whole-call send and receive timeout. pub const fn read_write_timeout(&self) -> Duration { self.read_write_timeout } /// Return the TCP_NODELAY override. pub const fn tcp_nodelay(&self) -> Option { self.tcp_nodelay } /// Return whether sockets request zero linger. pub const fn zero_linger(&self) -> bool { self.zero_linger } /// Return the network receive buffer size. pub const fn read_buffer_size(&self) -> usize { self.read_buffer_size } /// Return the storage directory retained by the runtime's storage resources. pub const fn storage_directory(&self) -> &PathBuf { &self.storage_directory } /// Return the accepted blob layout range. pub const fn storage_blob_layouts(&self) -> &RangeInclusive { &self.storage_blob_layouts } /// Validate and normalize settings before acquiring any startup resources. fn validate(&mut self) { assert!( (1..=MAX_WORKERS).contains(&self.worker_threads), "worker threads ({}) must be between 1 and {MAX_WORKERS}", self.worker_threads, ); assert!( self.global_queue_interval != 0, "global queue interval must be nonzero" ); assert!(self.ring_config.size != 0, "ring size must be nonzero"); self.ring_config.size = self .ring_config .size .checked_next_power_of_two() .expect("ring size overflow"); assert!(self.ring_config.size <= 32_768, "ring size exceeds 32768"); assert!( self.idle_spinner.budget_us <= self.idle_spinner.max_budget_us, "spinner budget_us ({}) must not exceed max_budget_us ({})", self.idle_spinner.budget_us, self.idle_spinner.max_budget_us, ); assert!( !self.storage_blob_layouts.is_empty(), "storage blob layouts must be non-empty" ); assert!( !self.connect_timeout.is_zero(), "connect timeout must be nonzero" ); assert!( !self.read_write_timeout.is_zero(), "read/write timeout must be nonzero" ); TimeoutWheel::validate_layout(self.max_timeout(), self.ring_config.timeout_wheel_tick) .expect("invalid io_uring operation deadline layout"); } /// Largest network timeout supported by every worker's operation wheel. fn max_timeout(&self) -> Duration { self.connect_timeout.max(self.read_write_timeout) } } impl Default for Config { fn default() -> Self { Self::new() } } /// Task counters shared by all workers in one runner. struct TaskMetrics { /// Number of tasks created, including rejected spawns. tasks_spawned: CounterFamily