Initial commit
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
||||
//! Listener pool and the `serve` entry point.
|
||||
//!
|
||||
//! A small fixed pool of listener actors share the same TCP listen fd (via
|
||||
//! `dup`) — each blocks in non-blocking `accept4` + `wait_readable` on its
|
||||
//! own copy. When a connection arrives the listener spawns a connection
|
||||
//! actor with the `OwnedFd` and immediately returns to `accept`. No
|
||||
//! coordination needed; the kernel serialises `accept` calls across the fds.
|
||||
//!
|
||||
//! Sharing via `dup` rather than the same fd is deliberate — Linux's
|
||||
//! `accept4` is thread-safe on a single fd, but dup'ing per-listener keeps
|
||||
//! each actor's epoll registration local to its own RawFd value (so smarm's
|
||||
//! `waiters: HashMap<RawFd, Pid>` doesn't see collisions between listeners
|
||||
//! waiting on "the same fd").
|
||||
|
||||
use crate::conn_actor::{run_connection, ConnLimits};
|
||||
use crate::net::{accept_nonblocking, bind_and_listen, OwnedFd};
|
||||
use crate::plug::Pipeline;
|
||||
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::os::fd::RawFd;
|
||||
use std::time::Duration;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub addr: SocketAddr,
|
||||
pub listener_pool: usize,
|
||||
pub keep_alive_timeout: Duration,
|
||||
pub max_header_count: usize,
|
||||
pub read_buf_size: usize,
|
||||
pub request_timeout: Duration,
|
||||
pub max_body_bytes: usize,
|
||||
/// Number of smarm scheduler OS threads. `None` means smarm's default
|
||||
/// (one per CPU). Set this to a small fixed number in tests so multiple
|
||||
/// concurrent test servers don't oversubscribe the host.
|
||||
pub scheduler_threads: Option<usize>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(addr: SocketAddr) -> Self {
|
||||
let pool = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(2)
|
||||
.max(2);
|
||||
Self {
|
||||
addr,
|
||||
listener_pool: pool,
|
||||
keep_alive_timeout: Duration::from_secs(60),
|
||||
max_header_count: 64,
|
||||
read_buf_size: 8 * 1024,
|
||||
request_timeout: Duration::from_secs(30),
|
||||
max_body_bytes: 16 * 1024 * 1024,
|
||||
scheduler_threads: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_conn_limits(&self) -> ConnLimits {
|
||||
ConnLimits {
|
||||
max_headers: self.max_header_count,
|
||||
initial_read_buf: self.read_buf_size,
|
||||
max_head_bytes: 64 * 1024,
|
||||
max_body_bytes: self.max_body_bytes,
|
||||
keep_alive_timeout: self.keep_alive_timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// dup helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn dup_fd(fd: RawFd) -> io::Result<OwnedFd> {
|
||||
let new_fd = unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) };
|
||||
if new_fd < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(OwnedFd::from_raw(new_fd))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// listener actor body
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn listener_loop(listener: OwnedFd, pipeline: Pipeline, limits: ConnLimits) {
|
||||
let fd = listener.as_raw();
|
||||
loop {
|
||||
match accept_nonblocking(fd) {
|
||||
Ok(client) => {
|
||||
// Hand the fd off to a new connection actor. spawn() is
|
||||
// cheap on smarm — it's a single Vec push under the
|
||||
// shared lock.
|
||||
let p = pipeline.clone();
|
||||
let l = limits;
|
||||
smarm::spawn(move || run_connection(client, p, l));
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
// 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.
|
||||
eprintln!("urus: listener wait_readable failed: {we}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() == ErrorKind::Interrupted => {
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
// EMFILE / ENFILE / ECONNABORTED etc. Log and continue;
|
||||
// the system may recover.
|
||||
eprintln!("urus: accept error: {e}");
|
||||
// Small backoff via smarm's sleep to avoid spinning if
|
||||
// the error is sticky.
|
||||
smarm::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
// listener OwnedFd drops here, closing the dup'd fd.
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// serve_with — main entry. Boots smarm, spawns 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.
|
||||
|
||||
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.
|
||||
let mut listener_fds = Vec::with_capacity(config.listener_pool);
|
||||
listener_fds.push(listener); // primary keeps the original
|
||||
|
||||
for _ in 1..config.listener_pool {
|
||||
let dup = dup_fd(listener_fds[0].as_raw())?;
|
||||
listener_fds.push(dup);
|
||||
}
|
||||
|
||||
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);
|
||||
for (i, lfd) in listener_fds.into_iter().enumerate() {
|
||||
let p = pipeline.clone();
|
||||
let h = smarm::spawn(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();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// serve — convenience over serve_with.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn serve(addr: impl ToSocketAddrs, pipeline: Pipeline) -> io::Result<()> {
|
||||
let addr = addr
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| io::Error::new(ErrorKind::InvalidInput, "no addresses resolved"))?;
|
||||
serve_with(Config::new(addr), pipeline)
|
||||
}
|
||||
Reference in New Issue
Block a user