feat(scheduler,supervisor,gen_server): graceful shutdown — request_shutdown, child Shutdown policy, handle_shutdown
Lift OTP's `exit(Pid, shutdown)` + child-spec `shutdown` wholesale.
scheduler / runtime
- `request_shutdown(pid)`: the polite stop. A target trapping exits gets an
`ExitSignal { reason: DownReason::Shutdown }` on its trap inbox and keeps
running; a non-trapping target is stopped as by `request_stop`, which is
now documented as the hard stop (`exit(Pid, kill)`). Dead pid: no-op.
- `RuntimeHandle::request_shutdown` for the off-runtime (signal thread) path;
`from == ROOT_PID` there.
- `DownReason::Shutdown` — appears only in ExitSignal, never in Down (a
complying target exits *normally*).
supervisor
- `ChildSpec::shutdown(Shutdown::{BrutalKill, Timeout(d), Infinity})`,
default Timeout(5s). Every supervisor-initiated stop (ordered shutdown and
OneForAll/RestForOne sibling cycling) is: request_shutdown → await the
child's Signal up to the grace → request_stop → await. Sequential, reverse
start order.
- The supervisor traps exits; a Shutdown ExitSignal runs the ordered
shutdown and `run()` returns normally, so `request_shutdown(root_sup)`
tears a whole tree down top-down with each child's grace period.
- FIX: a hard `request_stop` on a supervisor previously orphaned its
children (the ordered shutdown lived after the loop, and the unwind
skipped it). `Live` (the by_pid map) now carries a drop guard that
fire-and-forget hard-stops live children when unwinding.
gen_server
- `GenServerCtx::trap_exit()` opt-in in `init`; the trap inbox becomes arm 0
of the loop's select. Shutdown ExitSignal → `handle_shutdown() ->
ShutdownAction::{Exit, Continue}` (default Exit: loop breaks, `terminate`
runs on the normal path and may block). Other ExitSignals →
`handle_exit(sig)`.
- `GenServerCtx::stop_handle() -> StopHandle`, `stop()` ends the server
after the current message with a *normal* exit — the missing
`{stop, normal, State}`; `request_stop(self_pid())` was the only self-exit
and it is abnormal (Transient restarts it).
- `GenServerRef::shutdown()` / `gen_server::shutdown(name)` now go through
`request_shutdown`.
Tests: tests/shutdown.rs, tests/supervisor_shutdown.rs,
tests/gen_server_shutdown.rs. Full suite green; fmt + clippy --lib clean.
This commit is contained in:
+53
-4
@@ -600,10 +600,12 @@ pub(crate) fn retire_wait() {
|
||||
/// [`JoinHandle::join`] reports it as a normal, non-error exit: cooperative
|
||||
/// stop is a controlled shutdown, not a failure.
|
||||
///
|
||||
/// This is exactly the mechanism `gen_server` shutdown, supervisor restarts,
|
||||
/// and structured teardown are built from: reach for [`GenServerRef::shutdown`](crate::GenServerRef::shutdown)
|
||||
/// or a [`supervisor`](crate::supervisor) instead of calling this directly
|
||||
/// where those apply.
|
||||
/// This is the *hard* stop — OTP's `exit(Pid, kill)`. It is what a supervisor
|
||||
/// falls back to when a child overstays its [`Shutdown`](crate::supervisor::Shutdown)
|
||||
/// grace period. For a stop the target gets to prepare for, use
|
||||
/// [`request_shutdown`]; for structured teardown, reach for
|
||||
/// [`GenServerRef::shutdown`](crate::GenServerRef::shutdown) or a
|
||||
/// [`supervisor`](crate::supervisor) instead of calling this directly.
|
||||
///
|
||||
/// Because it's cooperative, an actor stuck in a tight loop with no
|
||||
/// blocking call, no [`check!`](crate::check), and no allocation cannot be
|
||||
@@ -635,6 +637,53 @@ pub(crate) fn request_stop_inner(inner: &RuntimeInner, pid: Pid) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask an actor to shut down gracefully — OTP's `exit(Pid, shutdown)`, where
|
||||
/// [`request_stop`] is `exit(Pid, kill)`.
|
||||
///
|
||||
/// If the target has called [`trap_exit`](crate::trap_exit), it receives an
|
||||
/// [`ExitSignal`](crate::ExitSignal) with reason
|
||||
/// [`DownReason::Shutdown`](crate::DownReason::Shutdown) on its trap inbox and
|
||||
/// keeps running: the request is advisory, and the target is expected to wind
|
||||
/// down and exit normally in its own time (a supervisor bounds that time with
|
||||
/// its child's [`Shutdown`](crate::supervisor::Shutdown) policy and falls back
|
||||
/// to `request_stop`). A target that is not trapping is stopped exactly as by
|
||||
/// `request_stop`. A dead pid is a no-op.
|
||||
///
|
||||
/// The signal's `from` is the calling actor, or `ROOT_PID` when driven from
|
||||
/// outside the runtime (see [`RuntimeHandle::request_shutdown`](crate::RuntimeHandle::request_shutdown)).
|
||||
pub fn request_shutdown<A>(pid: Pid<A>) {
|
||||
let pid = pid.erase();
|
||||
let from = current_pid().unwrap_or(crate::runtime::ROOT_PID);
|
||||
let _ = try_with_runtime(|inner| request_shutdown_inner(inner, pid, from));
|
||||
}
|
||||
|
||||
// The core of `request_shutdown`. Reads the target's trap sender under its
|
||||
// cold lock (generation-verified), then acts outside the lock: a trap send
|
||||
// may unpark the receiver, and `request_stop_inner` re-takes the lock.
|
||||
pub(crate) fn request_shutdown_inner(inner: &RuntimeInner, pid: Pid, from: Pid) {
|
||||
let trap = match inner.slot_at(pid) {
|
||||
Some(slot) => {
|
||||
let cold = slot.cold.lock();
|
||||
if slot.generation() == pid.generation() {
|
||||
cold.actor.as_ref().map(|a| a.trap.clone())
|
||||
} else {
|
||||
None // stale pid: nothing there to shut down
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
match trap {
|
||||
Some(Some(tx)) => {
|
||||
let _ = tx.send(crate::link::ExitSignal {
|
||||
from,
|
||||
reason: crate::monitor::DownReason::Shutdown,
|
||||
});
|
||||
}
|
||||
Some(None) => request_stop_inner(inner, pid),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NoPreempt
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user