feat(serve): supervised listener pool (v0.2 chunk 1)
OneForOne supervisor on the root actor, Restart::Permanent per listener. ChildSpec factory owns its fd via Arc<OwnedFd> (OwnedFd gains Sync); restarts reuse the fd — no re-dup, no fd-less window. wait_readable failure now exits-and-restarts instead of being fatal. Conn actors stay unsupervised bare spawns. Test hook INJECT_LISTENER_PANICS panics before accept so a pending connection must survive into the restarted listener. Roadmap: chunk 1 marked done; chunk 3 fork resolved to smarm-native timed waits (RFC 008 landed at 393cdd0, reaper option removed); chunk 2 drain-then-stop decided; v0.4-3(b) unblocked.
This commit is contained in:
@@ -52,6 +52,13 @@ impl Drop for OwnedFd {
|
||||
// becomes the unique closer.
|
||||
unsafe impl Send for OwnedFd {}
|
||||
|
||||
// SAFETY: shared references only expose `as_raw(&self)` — reading an
|
||||
// integer. No interior mutability; `into_raw` takes `self` by value and is
|
||||
// unreachable through a shared reference. Needed so a supervisor
|
||||
// `ChildSpec` factory (an `Fn` closure, hence shared across restarts) can
|
||||
// own its listener fd via `Arc<OwnedFd>`.
|
||||
unsafe impl Sync for OwnedFd {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// bind_and_listen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+58
-25
@@ -16,9 +16,13 @@ use crate::conn_actor::{run_connection, ConnLimits};
|
||||
use crate::net::{accept_nonblocking, bind_and_listen, OwnedFd};
|
||||
use crate::plug::Pipeline;
|
||||
|
||||
use smarm::{ChildSpec, OneForOne, Restart, Strategy};
|
||||
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -85,9 +89,24 @@ fn dup_fd(fd: RawFd) -> io::Result<OwnedFd> {
|
||||
// listener actor body
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn listener_loop(listener: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
|
||||
/// Test-only fault injection. When nonzero, the next accept-loop iteration
|
||||
/// of whichever listener gets there first decrements this and panics —
|
||||
/// *before* calling `accept`, so a pending connection stays in the kernel
|
||||
/// backlog and must be picked up by the restarted listener. Cost when idle
|
||||
/// is one relaxed load per accept-loop iteration (each of which already
|
||||
/// pays a syscall). Not public API.
|
||||
#[doc(hidden)]
|
||||
pub static INJECT_LISTENER_PANICS: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
fn listener_loop(listener: Arc<OwnedFd>, pipeline: Pipeline, limits: ConnLimits) {
|
||||
let fd = listener.as_raw();
|
||||
loop {
|
||||
if INJECT_LISTENER_PANICS
|
||||
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_sub(1))
|
||||
.is_ok()
|
||||
{
|
||||
panic!("urus: injected listener panic (test hook)");
|
||||
}
|
||||
match accept_nonblocking(fd) {
|
||||
Ok(client) => {
|
||||
// Hand the fd off to a new connection actor. spawn() is
|
||||
@@ -101,7 +120,11 @@ fn listener_loop(listener: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
|
||||
// No pending connection. Park until the listener is
|
||||
// readable again, then retry.
|
||||
if let Err(we) = smarm::wait_readable(fd) {
|
||||
// epoll registration failed — fatal for this listener.
|
||||
// epoll registration failed. Under supervision a
|
||||
// `return` is a `Signal::Exit` and `Restart::Permanent`
|
||||
// restarts us — a transient failure (e.g. EMFILE on
|
||||
// the epoll set) now heals instead of silently
|
||||
// shrinking the pool.
|
||||
eprintln!("urus: listener wait_readable failed: {we}");
|
||||
return;
|
||||
}
|
||||
@@ -119,57 +142,67 @@ fn listener_loop(listener: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// listener OwnedFd drops here, closing the dup'd fd.
|
||||
// The Arc clone we were started with drops here (normal exit or
|
||||
// unwind), but the ChildSpec factory holds another — the fd outlives
|
||||
// any one incarnation of this listener.
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// serve_with — main entry. Boots smarm, spawns listeners, blocks.
|
||||
// serve_with — main entry. Boots smarm, supervises listeners, blocks.
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// Boots an smarm runtime (one OS thread per CPU by default — see smarm's
|
||||
// `Config::default()`) and runs until externally killed. We don't wire a
|
||||
// graceful-shutdown signal in v1; the runtime exits when all actors exit,
|
||||
// which they don't (listener loops are infinite). Ctrl-C is your friend.
|
||||
// `Config::default()`) and runs a one-for-one supervisor over the listener
|
||||
// pool on the root actor. A panicking listener is restarted on the same
|
||||
// (still-open) fd instead of silently shrinking the accept pool. Connection
|
||||
// actors stay unsupervised bare spawns — per spec, a connection is cheap
|
||||
// and its failure is local: a 500 path, not a restart path. (`spawn` from a
|
||||
// listener parents the conn actor under that listener, which never
|
||||
// registers a supervisor channel, so conn deaths are invisible to the pool
|
||||
// supervisor by construction.)
|
||||
//
|
||||
// No graceful-shutdown signal yet (v0.2 chunk 2); the supervisor loop never
|
||||
// returns because listeners never reach a terminal state. Ctrl-C is your
|
||||
// friend.
|
||||
|
||||
pub fn serve_with(config: Config, pipeline: Pipeline) -> io::Result<()> {
|
||||
let listener = bind_and_listen(config.addr)?;
|
||||
println!("urus: listening on {}", config.addr);
|
||||
|
||||
// We want one connection-actor-spawning loop per listener pool slot.
|
||||
// Each gets its own dup'd fd so epoll registrations don't collide.
|
||||
// One connection-actor-spawning loop per listener pool slot. Each gets
|
||||
// its own dup'd fd so epoll registrations don't collide. Each fd is
|
||||
// owned by its ChildSpec's factory closure (via `Arc`): a restarted
|
||||
// listener re-enters `accept`/`wait_readable` on the same fd — no
|
||||
// re-dup, no window where the slot has no fd.
|
||||
let mut listener_fds = Vec::with_capacity(config.listener_pool);
|
||||
listener_fds.push(listener); // primary keeps the original
|
||||
listener_fds.push(Arc::new(listener)); // primary keeps the original
|
||||
|
||||
for _ in 1..config.listener_pool {
|
||||
let dup = dup_fd(listener_fds[0].as_raw())?;
|
||||
listener_fds.push(dup);
|
||||
listener_fds.push(Arc::new(dup));
|
||||
}
|
||||
|
||||
let limits = config.to_conn_limits();
|
||||
let limits = config.to_conn_limits();
|
||||
|
||||
// smarm's runtime API: init(Config) then run(f). The closure is the
|
||||
// root actor; from there we spawn one listener per fd in the pool.
|
||||
let smarm_cfg = match config.scheduler_threads {
|
||||
Some(n) => smarm::Config::exact(n),
|
||||
None => smarm::Config::default(),
|
||||
};
|
||||
let rt = smarm::init(smarm_cfg);
|
||||
rt.run(move || {
|
||||
let n = listener_fds.len();
|
||||
let mut handles = Vec::with_capacity(n);
|
||||
let mut sup = OneForOne::new().strategy(Strategy::OneForOne);
|
||||
for (i, lfd) in listener_fds.into_iter().enumerate() {
|
||||
let p = pipeline.clone();
|
||||
let h = smarm::spawn(move || {
|
||||
sup = sup.child(ChildSpec::new(Restart::Permanent, move || {
|
||||
println!("urus: listener {} starting", i);
|
||||
listener_loop(lfd, p, limits);
|
||||
});
|
||||
handles.push(h);
|
||||
}
|
||||
// Block forever (until ctrl-C) by joining the listeners. They
|
||||
// never exit on their own in v1.
|
||||
for h in handles {
|
||||
let _ = h.join();
|
||||
listener_loop(lfd.clone(), p.clone(), limits);
|
||||
}));
|
||||
}
|
||||
// Supervision loop on the root actor. Default intensity (3 per 5s)
|
||||
// applies per the runtime; a listener crash-looping faster than
|
||||
// that trips the cap and tears the pool down — loud failure over a
|
||||
// zombie server.
|
||||
sup.run();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user