use std::{fmt, future::Future, io, marker::PhantomData, panic, rc::Rc, sync::Arc, time};
use crate::{driver::Runner, signals, system::System, system::SystemConfig};
#[derive(Debug, Clone)]
pub struct Builder {
name: String,
stack_size: usize,
ping_interval: usize,
ping_threshold: usize,
latency_interval: usize,
signals: bool,
panics: bool,
pool_limit: usize,
pool_recv_timeout: time::Duration,
testing: bool,
}
impl Builder {
pub(super) fn new() -> Self {
Builder {
name: "ntex".into(),
stack_size: 0,
ping_interval: 2000,
ping_threshold: 1000,
latency_interval: 100,
signals: false,
panics: false,
testing: false,
pool_limit: 256,
pool_recv_timeout: time::Duration::from_mins(1),
}
}
#[must_use]
pub fn name<N: AsRef<str>>(mut self, name: N) -> Self {
self.name = name.as_ref().into();
self
}
#[must_use]
pub fn signals(mut self, eanbled: bool) -> Self {
self.signals = eanbled;
self
}
#[must_use]
pub fn panic_handling(mut self, eanbled: bool) -> Self {
self.panics = eanbled;
self
}
#[doc(hidden)]
#[must_use]
pub fn disable_signals(mut self) -> Self {
self.signals = false;
self
}
#[doc(hidden)]
#[must_use]
pub fn enable_signals(mut self) -> Self {
self.signals = true;
self
}
#[must_use]
pub fn stack_size(mut self, size: usize) -> Self {
self.stack_size = size;
self
}
#[must_use]
pub fn ping_interval(mut self, interval: usize) -> Self {
self.ping_interval = interval;
self
}
#[must_use]
pub fn ping_threshold(mut self, interval: usize) -> Self {
self.ping_threshold = interval;
self
}
#[must_use]
pub fn latency_interval(mut self, interval: usize) -> Self {
self.latency_interval = interval;
self
}
#[must_use]
pub fn thread_pool_limit(mut self, value: usize) -> Self {
self.pool_limit = value;
self
}
#[must_use]
pub fn testing(mut self) -> Self {
self.testing = true;
self.signals = false;
self.panics = false;
self
}
#[must_use]
pub fn thread_pool_recv_timeout<T>(mut self, timeout: T) -> Self
where
time::Duration: From<T>,
{
self.pool_recv_timeout = timeout.into();
self
}
pub fn build<R: Runner>(self, runner: R) -> SystemRunner {
let config = SystemConfig {
name: self.name.clone(),
testing: self.testing,
stack_size: self.stack_size,
ping_interval: self.ping_interval,
ping_threshold: self.ping_threshold,
latency_interval: self.latency_interval,
pool_limit: self.pool_limit,
pool_recv_timeout: self.pool_recv_timeout,
runner: Arc::new(runner),
};
self.build_with(config)
}
pub fn build_with(self, config: SystemConfig) -> SystemRunner {
let runner = config.runner.clone();
SystemRunner {
config,
runner,
signals: self.signals,
panics: self.panics,
_t: PhantomData,
}
}
}
#[must_use = "SystemRunner must be run"]
pub struct SystemRunner {
config: SystemConfig,
runner: Arc<dyn Runner>,
signals: bool,
panics: bool,
_t: PhantomData<Rc<()>>,
}
impl SystemRunner {
pub fn run_until_stop(self) -> io::Result<()> {
self.run(|| Ok(()))
}
pub fn run<F>(self, f: F) -> io::Result<()>
where
F: FnOnce() -> io::Result<()> + 'static,
{
log::info!("Starting {:?} system", self.config.name);
let SystemRunner {
config,
runner,
signals,
panics,
..
} = self;
if panics {
signals::enable_panic_handling();
}
crate::driver::block_on_panic(runner.as_ref(), async move {
let (system, stop) = System::start(config);
let _guard = SystemGuard(system.clone());
if signals {
system.enable_signals();
}
f()?;
let result = stop.await;
match result {
Ok(code) => {
if code != 0 {
Err(io::Error::other(format!("Non-zero exit code: {code}")))
} else {
Ok(())
}
}
Err(_) => Err(io::Error::other("Closed")),
}
})
}
#[allow(clippy::missing_panics_doc)]
pub fn block_on<F, R>(self, fut: F) -> R
where
F: Future<Output = R> + 'static,
R: 'static,
{
let SystemRunner {
config,
runner,
signals,
panics,
..
} = self;
if panics {
signals::enable_panic_handling();
}
crate::driver::block_on_panic(runner.as_ref(), async move {
let (system, _) = System::start(config);
let _guard = SystemGuard(system.clone());
if signals {
system.enable_signals();
}
let loc = current_location();
ntex_error::set_backtrace_start(loc.file(), loc.line() + 2);
fut.await
})
}
#[cfg(feature = "tokio")]
pub async fn run_local<F, R>(self, fut: F) -> R
where
F: Future<Output = R> + 'static,
R: 'static,
{
let SystemRunner { config, .. } = self;
let result = tok_io::task::LocalSet::new()
.run_until(async move {
let (system, _) = System::start(config);
let _guard = SystemGuard(system);
let loc = current_location();
ntex_error::set_backtrace_start(loc.file(), loc.line() + 2);
fut.await
})
.await;
crate::arbiter::run_shutdown_callbacks();
unsafe {
crate::remove_all_items();
}
if let Some(arb) = crate::Arbiter::try_current() {
arb.finalize();
}
result
}
}
struct SystemGuard(System);
impl Drop for SystemGuard {
fn drop(&mut self) {
self.0.disable_signals();
if std::thread::panicking() {
let list = self.0.take_arbiters();
if !list.is_empty() {
let _ = std::thread::Builder::new()
.spawn(move || list.into_iter().for_each(|arb| arb.stop()))
.map(std::thread::JoinHandle::join);
}
} else {
self.0.stop_arbiters();
}
}
}
#[track_caller]
pub(crate) fn current_location() -> &'static panic::Location<'static> {
panic::Location::caller()
}
impl fmt::Debug for SystemRunner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SystemRunner")
.field("config", &self.config)
.finish()
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{cell::Cell, cell::RefCell, time::Duration};
use super::*;
use crate::{Arbiter, testing::TestRunner};
#[test]
fn builder_options() {
let builder = System::build()
.name("opts")
.signals(false)
.panic_handling(false)
.enable_signals()
.disable_signals()
.stack_size(2 * 1024 * 1024)
.ping_interval(0)
.ping_threshold(10)
.thread_pool_limit(1)
.thread_pool_recv_timeout(Duration::from_millis(100))
.testing();
assert!(format!("{builder:?}").contains("opts"));
let runner = builder.build(TestRunner);
assert!(format!("{runner:?}").contains("opts"));
runner.block_on(async {
let sys = System::current();
assert_eq!(sys.name(), "opts");
assert!(sys.testing());
assert!(!sys.signals());
let mut arb = Arbiter::new();
arb.stop();
arb.join().unwrap();
assert_eq!(sys.spawn_blocking(|| 1).await, Ok(1));
});
}
#[test]
fn run_exit_codes() {
let err = System::new("test", TestRunner)
.run(|| Err(io::Error::other("init failed")))
.unwrap_err();
assert_eq!(err.to_string(), "init failed");
System::new("test", TestRunner)
.run(|| {
System::current().stop();
Ok(())
})
.unwrap();
let called = Rc::new(Cell::new(false));
let arb = Rc::new(RefCell::new(None));
let (called2, arb2) = (called.clone(), arb.clone());
let err = System::new("test", TestRunner)
.run(move || {
Arbiter::on_shutdown(move || called2.set(true));
*arb2.borrow_mut() = Some(Arbiter::new());
System::current().stop_with_code(3);
Ok(())
})
.unwrap_err();
assert_eq!(err.to_string(), "Non-zero exit code: 3");
assert!(called.get());
let mut arb = arb.borrow_mut().take().unwrap();
arb.join().unwrap();
assert!(!arb.is_running());
}
#[test]
fn exit_stops_child_arbiters() {
fn assert_stopped(arb: &Rc<RefCell<Option<Arbiter>>>) {
let arb = arb.borrow_mut().take().unwrap();
let mut arb2 = arb.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(arb2.join().is_ok());
});
let stopped = rx.recv_timeout(Duration::from_secs(5));
arb.stop();
assert_eq!(stopped, Ok(true), "child arbiter is still running");
}
let arb = Rc::new(RefCell::new(None));
let arb2 = arb.clone();
let err = System::new("test", TestRunner)
.run(move || {
*arb2.borrow_mut() = Some(Arbiter::new());
Err(io::Error::other("init failed"))
})
.unwrap_err();
assert_eq!(err.to_string(), "init failed");
assert_stopped(&arb);
let arb2 = arb.clone();
System::new("test", TestRunner).block_on(async move {
*arb2.borrow_mut() = Some(Arbiter::new());
});
assert_stopped(&arb);
let arb2 = arb.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
System::new("test", TestRunner).block_on(async move {
*arb2.borrow_mut() = Some(Arbiter::new());
panic!("root future panic");
});
}));
assert!(result.is_err());
assert_stopped(&arb);
}
#[test]
fn primary_arbiter_finalized() {
fn with_on_stop(calls: &Arc<AtomicUsize>) -> Arbiter {
let calls = calls.clone();
Arbiter::current().on_stop(move || {
calls.fetch_add(1, Ordering::Relaxed);
})
}
let calls = Arc::new(AtomicUsize::new(0));
let calls2 = calls.clone();
let arb = System::new("test", TestRunner).block_on(async move { with_on_stop(&calls2) });
assert!(!arb.is_running());
assert_eq!(calls.load(Ordering::Relaxed), 1);
let calls = Arc::new(AtomicUsize::new(0));
let (calls2, slot) = (calls.clone(), Rc::new(RefCell::new(None)));
let slot2 = slot.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
System::new("test", TestRunner).block_on(async move {
*slot2.borrow_mut() = Some(with_on_stop(&calls2));
panic!("root future panic");
});
}));
assert!(result.is_err());
assert!(!slot.borrow().as_ref().unwrap().is_running());
assert_eq!(calls.load(Ordering::Relaxed), 1);
let calls = Arc::new(AtomicUsize::new(0));
let (calls2, slot) = (calls.clone(), Rc::new(RefCell::new(None)));
let slot2 = slot.clone();
System::new("test", TestRunner)
.run(move || {
*slot2.borrow_mut() = Some(with_on_stop(&calls2));
System::current().stop();
Ok(())
})
.unwrap();
assert!(!slot.borrow().as_ref().unwrap().is_running());
assert_eq!(calls.load(Ordering::Relaxed), 1);
}
#[cfg(feature = "tokio")]
#[test]
fn run_local_finalizes_primary_arbiter() {
let calls = Arc::new(AtomicUsize::new(0));
let calls2 = calls.clone();
let rt = tok_io::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let arb = rt.block_on(System::new("test", TestRunner).run_local(async move {
Arbiter::current().on_stop(move || {
calls2.fetch_add(1, Ordering::Relaxed);
})
}));
assert!(!arb.is_running());
assert_eq!(calls.load(Ordering::Relaxed), 1);
}
}