feat(serve): give connection actors a 256 KiB stack via smarm SpawnOpts

Connection actors were still spawned with a bare smarm::spawn(), which
gets the runtime's fixed 64 KiB default stack regardless of smarm
v0.6.0's RFC 019 SpawnOpts/stack_reserve work landing one crate down.
Any handler that leans on app code with real stack needs (DB drivers,
(de)compression, ...) blows the guard page and the connection just
dies with no response - reproduced with a CCC handler that decompresses
gzip on the identity-encoding path.

Add Config::conn_stack_reserve (default DEFAULT_CONN_STACK_RESERVE =
256 KiB) and thread it through listener_loop into a
smarm::spawn_with(SpawnOpts { stack_reserve: Some(_), .. }, ...) call
for every accepted connection. Existing Config { ..Config::new(addr) }
call sites (tests/integration.rs) pick up the new field automatically
via struct-update syntax; no call-site churn beyond that.

Bump to 0.2.2.
This commit is contained in:
2026-08-08 22:39:37 +02:00
parent b137c646b0
commit ab8e833460
2 changed files with 23 additions and 5 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "urus" name = "urus"
version = "0.2.1" version = "0.2.2"
edition = "2021" edition = "2021"
rust-version = "1.95" rust-version = "1.95"
description = "Cowboy/bandit-style HTTP library for the smarm actor runtime" description = "Cowboy/bandit-style HTTP library for the smarm actor runtime"
+20 -2
View File
@@ -57,8 +57,19 @@ pub struct Config {
/// (one per CPU). Set this to a small fixed number in tests so multiple /// (one per CPU). Set this to a small fixed number in tests so multiple
/// concurrent test servers don't oversubscribe the host. /// concurrent test servers don't oversubscribe the host.
pub scheduler_threads: Option<usize>, pub scheduler_threads: Option<usize>,
/// Stack reserve (RFC 019 `smarm::SpawnOpts::stack_reserve`) given to
/// each per-connection actor. Request handlers routinely pull in
/// application code — DB drivers, (de)compression, templating — whose
/// stack needs comfortably exceed smarm's bare-actor default of 64 KiB
/// (the exact shape of bug this exists to head off; see smarm RFC 019).
/// Default: 256 KiB. The reserve is virtual/demand-paged, so raising it
/// costs address space, not RSS, until a handler actually uses it.
pub conn_stack_reserve: usize,
} }
/// Default per-connection actor stack reserve (see [`Config::conn_stack_reserve`]).
pub const DEFAULT_CONN_STACK_RESERVE: usize = 256 * 1024;
impl Config { impl Config {
pub fn new(addr: SocketAddr) -> Self { pub fn new(addr: SocketAddr) -> Self {
let pool = std::thread::available_parallelism() let pool = std::thread::available_parallelism()
@@ -78,6 +89,7 @@ impl Config {
max_frame_payload: 1024 * 1024, max_frame_payload: 1024 * 1024,
max_message_bytes: 4 * 1024 * 1024, max_message_bytes: 4 * 1024 * 1024,
scheduler_threads: None, scheduler_threads: None,
conn_stack_reserve: DEFAULT_CONN_STACK_RESERVE,
} }
} }
@@ -125,6 +137,7 @@ fn listener_loop(
listener: Arc<OwnedFd>, listener: Arc<OwnedFd>,
pipeline: Pipeline, pipeline: Pipeline,
limits: ConnLimits, limits: ConnLimits,
conn_stack_reserve: usize,
registry: GenServerRef<ConnRegistry>, registry: GenServerRef<ConnRegistry>,
shutdown: Arc<AtomicBool>, shutdown: Arc<AtomicBool>,
) { ) {
@@ -157,7 +170,11 @@ fn listener_loop(
let p = pipeline.clone(); let p = pipeline.clone();
let l = limits; let l = limits;
let r = registry.clone(); let r = registry.clone();
smarm::spawn(move || run_connection(client, p, l, r)); let opts = smarm::SpawnOpts {
stack_reserve: Some(conn_stack_reserve),
..smarm::SpawnOpts::default()
};
smarm::spawn_with(opts, move || run_connection(client, p, l, r));
} }
Err(e) if e.kind() == ErrorKind::WouldBlock => { Err(e) if e.kind() == ErrorKind::WouldBlock => {
// No pending connection. Park until the listener is // No pending connection. Park until the listener is
@@ -306,6 +323,7 @@ pub fn serve_with_shutdown(
} }
let limits = config.to_conn_limits(); let limits = config.to_conn_limits();
let conn_stack_reserve = config.conn_stack_reserve;
let drain_timeout = config.drain_timeout; let drain_timeout = config.drain_timeout;
let smarm_cfg = match config.scheduler_threads { let smarm_cfg = match config.scheduler_threads {
@@ -327,7 +345,7 @@ pub fn serve_with_shutdown(
let sf = shutdown_flag.clone(); let sf = shutdown_flag.clone();
sup = sup.child(ChildSpec::new(Restart::Transient, move || { sup = sup.child(ChildSpec::new(Restart::Transient, move || {
println!("urus: listener {} starting", i); println!("urus: listener {} starting", i);
listener_loop(lfd.clone(), p.clone(), limits, r.clone(), sf.clone()); listener_loop(lfd.clone(), p.clone(), limits, conn_stack_reserve, r.clone(), sf.clone());
})); }));
} }
// Default intensity (3 per 5s) applies; a listener crash-looping // Default intensity (3 per 5s) applies; a listener crash-looping