rust_web_server/server/
per_core.rs1use std::borrow::Cow;
19use std::cell::Cell;
20use std::net::SocketAddr;
21
22use tokio::sync::watch;
23
24pub const RWS_CONFIG_RUNTIME: &str = "RWS_CONFIG_RUNTIME";
26
27thread_local! {
28 static SLOT: Cell<usize> = const { Cell::new(0) };
30}
31
32pub fn enabled() -> bool {
35 cfg!(unix)
36 && std::env::var(RWS_CONFIG_RUNTIME).is_ok_and(|v| v.trim().eq_ignore_ascii_case("per-core"))
37}
38
39pub fn default_to_per_core() {
51 if std::env::var_os(RWS_CONFIG_RUNTIME).is_none() {
52 if pkcs11_key_configured() {
53 println!(" runtime: shared (a TLS key is on a PKCS#11 token; set runtime = \"per-core\" to override)");
54 std::env::set_var(RWS_CONFIG_RUNTIME, "shared");
55 } else {
56 std::env::set_var(RWS_CONFIG_RUNTIME, "per-core");
57 }
58 crate::server_config::invalidate();
59 }
60}
61
62fn pkcs11_key_configured() -> bool {
66 let named = |v: &str| v.trim_start_matches(['"', '\'', ' ']).starts_with("pkcs11:");
67 let in_env = std::env::vars().any(|(k, v)| k.starts_with("RWS_CONFIG_") && k.ends_with("KEY_FILE") && named(&v));
68 let in_args = std::env::args().any(|a| a.split_once('=').is_some_and(|(_, v)| named(v)));
69 let file = std::env::var("RWS_CONFIG_FILE").ok().filter(|p| !p.trim().is_empty()).unwrap_or_else(|| "rws.config.toml".to_string());
70 let in_file = std::fs::read_to_string(file).is_ok_and(|text| mentions_pkcs11_key(&text));
71 in_env || in_args || in_file
72}
73
74pub(crate) fn mentions_pkcs11_key(text: &str) -> bool {
76 text.lines().any(|line| {
77 line.split_once('=').is_some_and(|(k, v)| k.trim().ends_with("key_file") && v.trim().trim_start_matches(['"', '\'']).starts_with("pkcs11:"))
78 })
79}
80
81pub fn pkcs11_warning() -> Option<&'static str> {
84 let key_on_token = std::env::vars().any(|(k, v)| k.starts_with("RWS_CONFIG_") && k.ends_with("KEY_FILE") && v.trim().starts_with("pkcs11:"));
85 (enabled() && key_on_token).then_some("[runtime] per-core with a PKCS#11 TLS key: each handshake's signature blocks that core's connections while the token works; runtime = \"shared\" moves the wait off the worker")
86}
87
88pub fn runtime_count() -> usize {
91 std::env::var("TOKIO_WORKER_THREADS")
92 .ok()
93 .and_then(|v| v.trim().parse::<usize>().ok())
94 .filter(|n| *n > 0)
95 .unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1))
96}
97
98pub(crate) fn pool_key(key: &str) -> Cow<'_, str> {
102 match SLOT.with(|s| s.get()) {
103 0 => Cow::Borrowed(key),
104 slot => {
106 let slot = crate::ext::decimal::Decimal::new(slot as u128);
107 let mut owned = String::with_capacity(key.len() + 1 + slot.as_str().len());
108 owned.push_str(key);
109 owned.push('#');
110 owned.push_str(slot.as_str());
111 Cow::Owned(owned)
112 }
113 }
114}
115
116#[cfg(unix)]
119pub(crate) fn bind(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
120 if let Some(listener) = crate::systemd::tcp(addr, None) {
123 listener.set_nonblocking(true)?;
124 return tokio::net::TcpListener::from_std(listener);
125 }
126 let socket = if addr.is_ipv4() { tokio::net::TcpSocket::new_v4()? } else { tokio::net::TcpSocket::new_v6()? };
127 socket.set_reuseaddr(true)?;
128 socket.set_reuseport(true)?;
129 socket.bind(addr)?;
130 socket.listen(1024)
131}
132
133#[cfg(unix)]
141pub(crate) fn spawn<F, Fut>(
142 addr: SocketAddr,
143 count: usize,
144 stop: watch::Receiver<bool>,
145 accept_loop: F,
146) -> Vec<std::thread::JoinHandle<()>>
147where
148 F: Fn(tokio::net::TcpListener, watch::Receiver<bool>) -> Fut + Send + Clone + 'static,
149 Fut: std::future::Future<Output = ()>,
150{
151 if let Some(warning) = pkcs11_warning() {
152 eprintln!("{warning}");
153 }
154 let mut handles = Vec::with_capacity(count);
155 let (bound_tx, bound_rx) = std::sync::mpsc::channel::<()>();
156 for slot in 1..=count {
157 let bound = bound_tx.clone();
158 let stop = stop.clone();
159 let accept_loop = accept_loop.clone();
160 let spawned = std::thread::Builder::new().name(format!("rws-core-{}", slot)).spawn(move || {
161 SLOT.with(|s| s.set(slot));
162 let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
163 Ok(r) => r,
164 Err(e) => return eprintln!("per-core runtime {}: {}", slot, e),
165 };
166 runtime.block_on(async move {
167 let listener = bind(addr);
168 drop(bound);
169 match listener {
170 Ok(listener) => accept_loop(listener, stop).await,
171 Err(e) => eprintln!("per-core runtime {}: unable to listen on {}: {}", slot, addr, e),
172 }
173 });
174 });
175 match spawned {
176 Ok(h) => handles.push(h),
177 Err(e) => eprintln!("per-core runtime {}: {}", slot, e),
178 }
179 }
180 drop(bound_tx);
183 let _ = bound_rx.recv_timeout(std::time::Duration::from_secs(5));
184 handles
185}
186
187pub(crate) async fn stopped(stop: &mut watch::Receiver<bool>) {
189 let _ = stop.wait_for(|s| *s).await;
190}
191
192#[cfg(test)]
193mod tests {
194
195 #[test]
196 fn pkcs11_keys_are_spotted_in_config_text() {
197 use super::mentions_pkcs11_key;
198 assert!(mentions_pkcs11_key("tls_key_file = \"pkcs11:token=rws;object=k?module-path=/x.so\""));
199 assert!(mentions_pkcs11_key("[[virtual_host]]\ndomain = \"a\"\nkey_file = 'pkcs11:object=k'"));
200 assert!(!mentions_pkcs11_key("tls_key_file = \"/etc/rws/key.pem\""));
201 assert!(!mentions_pkcs11_key("comment = \"pkcs11:object=k\""));
202 assert!(!mentions_pkcs11_key(""));
203 }
204
205 use super::*;
206
207 #[cfg(unix)]
208 #[test]
209 fn the_port_accepts_connections_when_spawn_returns() {
210 let addr = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap();
211 let (stop_tx, stop_rx) = watch::channel(false);
212 let handles = spawn(addr, 2, stop_rx, |listener: tokio::net::TcpListener, mut stop: watch::Receiver<bool>| async move {
213 tokio::select! {
214 _ = async { while listener.accept().await.is_ok() {} } => {}
215 _ = stopped(&mut stop) => {}
216 }
217 });
218 assert_eq!(2, handles.len());
219 std::net::TcpStream::connect(addr).expect("listening as soon as spawn returns");
220 stop_tx.send(true).unwrap();
221 for handle in handles {
222 handle.join().unwrap();
223 }
224 }
225
226 #[test]
227 fn pool_key_is_shared_off_the_per_core_runtimes() {
228 assert!(matches!(pool_key("backend:80"), Cow::Borrowed("backend:80")));
229 }
230
231 #[test]
232 fn pool_key_is_per_runtime_on_a_per_core_thread() {
233 let keys = std::thread::spawn(|| {
234 SLOT.with(|s| s.set(3));
235 pool_key("backend:80").into_owned()
236 })
237 .join()
238 .unwrap();
239 assert_eq!("backend:80#3", keys);
240 }
241
242 #[test]
243 fn enabled_only_for_per_core() {
244 let _g = crate::test_env::lock();
245 let before = std::env::var(RWS_CONFIG_RUNTIME).ok();
246 std::env::remove_var(RWS_CONFIG_RUNTIME);
247 assert!(!enabled());
248 std::env::set_var(RWS_CONFIG_RUNTIME, "shared");
249 assert!(!enabled());
250 std::env::set_var(RWS_CONFIG_RUNTIME, " Per-Core ");
251 assert_eq!(cfg!(unix), enabled());
252 match before {
253 Some(v) => std::env::set_var(RWS_CONFIG_RUNTIME, v),
254 None => std::env::remove_var(RWS_CONFIG_RUNTIME),
255 }
256 }
257
258 #[test]
259 fn the_binary_defaults_to_per_core_unless_configured() {
260 let _g = crate::test_env::lock();
261 let before = std::env::var(RWS_CONFIG_RUNTIME).ok();
262 std::env::remove_var(RWS_CONFIG_RUNTIME);
263 default_to_per_core();
264 assert_eq!(Some("per-core".to_string()), std::env::var(RWS_CONFIG_RUNTIME).ok());
265 std::env::set_var(RWS_CONFIG_RUNTIME, "shared");
266 default_to_per_core();
267 assert_eq!(Some("shared".to_string()), std::env::var(RWS_CONFIG_RUNTIME).ok());
268 match before {
269 Some(v) => std::env::set_var(RWS_CONFIG_RUNTIME, v),
270 None => std::env::remove_var(RWS_CONFIG_RUNTIME),
271 }
272 }
273
274 #[test]
275 fn runtime_count_follows_tokio_worker_threads() {
276 let _g = crate::test_env::lock();
277 let before = std::env::var("TOKIO_WORKER_THREADS").ok();
278 std::env::set_var("TOKIO_WORKER_THREADS", "3");
279 assert_eq!(3, runtime_count());
280 std::env::set_var("TOKIO_WORKER_THREADS", "zero");
281 assert!(runtime_count() >= 1);
282 match before {
283 Some(v) => std::env::set_var("TOKIO_WORKER_THREADS", v),
284 None => std::env::remove_var("TOKIO_WORKER_THREADS"),
285 }
286 }
287
288 #[cfg(unix)]
291 #[test]
292 fn runtimes_share_the_port_and_stop() {
293 use std::io::{Read, Write};
294 use tokio::io::AsyncWriteExt;
295
296 let addr = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap();
298 let (stop_tx, stop_rx) = watch::channel(false);
299 let accept_loop = |listener: tokio::net::TcpListener, mut stop: watch::Receiver<bool>| async move {
300 loop {
301 tokio::select! {
302 result = listener.accept() => {
303 if let Ok((mut stream, _)) = result {
304 let slot = SLOT.with(|s| s.get());
305 let _ = stream.write_all(slot.to_string().as_bytes()).await;
306 }
307 }
308 _ = stopped(&mut stop) => break,
309 }
310 }
311 };
312 let handles = spawn(addr, 2, stop_rx, accept_loop);
313 assert_eq!(2, handles.len());
314
315 let mut answered = 0;
316 for _ in 0..50 {
317 let Ok(mut c) = std::net::TcpStream::connect(addr) else {
318 std::thread::sleep(std::time::Duration::from_millis(20)); continue;
320 };
321 let _ = c.write_all(b"x");
322 let mut slot = String::new();
323 let _ = c.read_to_string(&mut slot);
324 if !slot.is_empty() {
325 assert!(slot == "1" || slot == "2", "slot {slot}");
326 answered += 1;
327 }
328 }
329 assert!(answered > 0);
330
331 stop_tx.send(true).unwrap();
332 for h in handles {
333 h.join().unwrap();
334 }
335 assert!(std::net::TcpStream::connect(addr).is_err());
336 }
337
338 #[cfg(unix)]
339 #[test]
340 fn bind_reports_an_address_in_use_without_reuseport() {
341 let taken = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
342 let addr = taken.local_addr().unwrap();
343 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
344 runtime.block_on(async { assert!(bind(addr).is_err()) });
345 }
346}