//! Graceful shutdown — `request_shutdown` (OTP `exit(Pid, shutdown)`). //! //! `request_stop` is `exit(Pid, kill)`: an uncatchable unwind at the target's //! next observation point. `request_shutdown` is the polite form: //! - a target that is NOT trapping exits is stopped exactly as by //! `request_stop` (OTP's rule: don't trap, you die); //! - a target that IS trapping receives an `ExitSignal { reason: Shutdown }` //! on its trap inbox and keeps running — it is expected to wind down and //! exit normally on its own. use smarm::{monitor, request_shutdown, run, self_pid, sleep, spawn, trap_exit, DownReason, Pid}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{mpsc, Arc}; use std::thread; use std::time::Duration; const WATCHDOG: Duration = Duration::from_secs(10); #[test] fn request_shutdown_stops_a_non_trapping_actor() { run(|| { let h = spawn(|| sleep(Duration::from_secs(3600))); let mon = monitor(h.pid()); request_shutdown(h.pid()); let down = mon.rx.recv().expect("down"); assert_eq!(down.reason, DownReason::Stopped); }); } #[test] fn request_shutdown_is_a_message_to_a_trapping_actor() { let unwound = Arc::new(AtomicBool::new(false)); let u = unwound.clone(); run(move || { struct Unwound(Arc); impl Drop for Unwound { fn drop(&mut self) { if std::thread::panicking() { self.0.store(true, Ordering::SeqCst); } } } let (tx, rx) = smarm::channel::<(Pid, DownReason)>(); let (ready_tx, ready_rx) = smarm::channel::<()>(); let h = spawn(move || { let _g = Unwound(u); let inbox = trap_exit(); let _ = ready_tx.send(()); let sig = inbox.recv().expect("exit signal"); let _ = tx.send((sig.from, sig.reason)); // Keep doing work after the request: shutdown is advisory. sleep(Duration::from_millis(20)); }); // Trapping is set by the target itself; a request that beats it is a // plain stop (same window as OTP's exit-before-process_flag). ready_rx.recv().expect("ready"); let me = self_pid(); let mon = monitor(h.pid()); request_shutdown(h.pid()); let (from, reason) = rx.recv().expect("relayed"); assert_eq!(from, me); assert_eq!(reason, DownReason::Shutdown); let down = mon.rx.recv().expect("down"); assert_eq!( down.reason, DownReason::Exit, "target exited normally, not stopped" ); }); assert!( !unwound.load(Ordering::SeqCst), "trapping target must not be unwound" ); } #[test] fn request_shutdown_on_dead_pid_is_a_no_op() { run(|| { let h = spawn(|| {}); let pid = h.pid(); let _ = h.join(); request_shutdown(pid); // must not panic }); } #[test] fn handle_request_shutdown_from_foreign_thread() { let rt = smarm::init(smarm::Config::exact(2)); let handle = rt.handle(); let (pid_tx, pid_rx) = mpsc::channel::(); let requester = thread::spawn(move || { let pid = pid_rx.recv().expect("pid"); thread::sleep(Duration::from_millis(50)); handle.request_shutdown(pid); }); let (done_tx, done_rx) = mpsc::channel(); thread::spawn(move || { rt.run(move || { let (tx, rx) = smarm::channel::(); let (ready_tx, ready_rx) = smarm::channel::<()>(); let h = spawn(move || { let inbox = trap_exit(); let _ = ready_tx.send(()); let sig = inbox.recv().expect("exit signal"); let _ = tx.send(sig.reason); }); ready_rx.recv().expect("ready"); pid_tx.send(h.pid()).expect("send pid"); let reason = rx.recv().expect("relayed"); assert_eq!(reason, DownReason::Shutdown); let _ = h.join(); }); let _ = done_tx.send(()); }); done_rx .recv_timeout(WATCHDOG) .expect("run did not return: foreign-thread request_shutdown never reached the target"); requester.join().expect("requester thread"); }