From 535f7bcc687bcdf4ef5cf366177ac166aa8466a1 Mon Sep 17 00:00:00 2001 From: Mark Kalsbeek Date: Sat, 8 Aug 2026 22:39:37 +0200 Subject: [PATCH] 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. --- Cargo.toml | 2 +- src/serve.rs | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 08e2799..4f4a4c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "urus" -version = "0.2.1" +version = "0.2.2" edition = "2021" rust-version = "1.95" description = "Cowboy/bandit-style HTTP library for the smarm actor runtime" diff --git a/src/serve.rs b/src/serve.rs index 2b08e2d..e6c0d25 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -57,8 +57,19 @@ pub struct Config { /// (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, + /// 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 { pub fn new(addr: SocketAddr) -> Self { let pool = std::thread::available_parallelism() @@ -78,6 +89,7 @@ impl Config { max_frame_payload: 1024 * 1024, max_message_bytes: 4 * 1024 * 1024, scheduler_threads: None, + conn_stack_reserve: DEFAULT_CONN_STACK_RESERVE, } } @@ -125,6 +137,7 @@ fn listener_loop( listener: Arc, pipeline: Pipeline, limits: ConnLimits, + conn_stack_reserve: usize, registry: GenServerRef, shutdown: Arc, ) { @@ -157,7 +170,11 @@ fn listener_loop( let p = pipeline.clone(); let l = limits; 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 => { // No pending connection. Park until the listener is @@ -305,8 +322,9 @@ pub fn serve_with_shutdown( listener_fds.push(Arc::new(dup)); } - let limits = config.to_conn_limits(); - let drain_timeout = config.drain_timeout; + let limits = config.to_conn_limits(); + let conn_stack_reserve = config.conn_stack_reserve; + let drain_timeout = config.drain_timeout; let smarm_cfg = match config.scheduler_threads { Some(n) => smarm::Config::exact(n), @@ -327,7 +345,7 @@ pub fn serve_with_shutdown( let sf = shutdown_flag.clone(); sup = sup.child(ChildSpec::new(Restart::Transient, move || { 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