Initial commit

This commit is contained in:
2026-05-26 23:16:45 +02:00
commit 3b6c466210
12 changed files with 2254 additions and 0 deletions
+188
View File
@@ -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)
}