rws_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
74#[doc(hidden)]
76pub fn mentions_pkcs11_key(text: &str) -> bool {
77 text.lines().any(|line| {
78 line.split_once('=').is_some_and(|(k, v)| k.trim().ends_with("key_file") && v.trim().trim_start_matches(['"', '\'']).starts_with("pkcs11:"))
79 })
80}
81
82pub fn pkcs11_warning() -> Option<&'static str> {
85 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:"));
86 (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")
87}
88
89pub fn runtime_count() -> usize {
92 std::env::var("TOKIO_WORKER_THREADS")
93 .ok()
94 .and_then(|v| v.trim().parse::<usize>().ok())
95 .filter(|n| *n > 0)
96 .unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1))
97}
98
99#[doc(hidden)]
103pub fn pool_key(key: &str) -> Cow<'_, str> {
104 match SLOT.with(|s| s.get()) {
105 0 => Cow::Borrowed(key),
106 slot => {
108 let slot = crate::ext::decimal::Decimal::new(slot as u128);
109 let mut owned = String::with_capacity(key.len() + 1 + slot.as_str().len());
110 owned.push_str(key);
111 owned.push('#');
112 owned.push_str(slot.as_str());
113 Cow::Owned(owned)
114 }
115 }
116}
117
118#[cfg(unix)]
121#[doc(hidden)]
122pub fn bind(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
123 if let Some(listener) = crate::systemd::tcp(addr, None) {
126 listener.set_nonblocking(true)?;
127 return tokio::net::TcpListener::from_std(listener);
128 }
129 let socket = if addr.is_ipv4() { tokio::net::TcpSocket::new_v4()? } else { tokio::net::TcpSocket::new_v6()? };
130 socket.set_reuseaddr(true)?;
131 socket.set_reuseport(true)?;
132 socket.bind(addr)?;
133 socket.listen(1024)
134}
135
136#[cfg(unix)]
144#[doc(hidden)]
145pub fn spawn<F, Fut>(
146 addr: SocketAddr,
147 count: usize,
148 stop: watch::Receiver<bool>,
149 accept_loop: F,
150) -> Vec<std::thread::JoinHandle<()>>
151where
152 F: Fn(tokio::net::TcpListener, watch::Receiver<bool>) -> Fut + Send + Clone + 'static,
153 Fut: std::future::Future<Output = ()>,
154{
155 if let Some(warning) = pkcs11_warning() {
156 eprintln!("{warning}");
157 }
158 let mut handles = Vec::with_capacity(count);
159 let (bound_tx, bound_rx) = std::sync::mpsc::channel::<()>();
160 for slot in 1..=count {
161 let bound = bound_tx.clone();
162 let stop = stop.clone();
163 let accept_loop = accept_loop.clone();
164 let spawned = std::thread::Builder::new().name(format!("rws-core-{}", slot)).spawn(move || {
165 SLOT.with(|s| s.set(slot));
166 let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
167 Ok(r) => r,
168 Err(e) => return eprintln!("per-core runtime {}: {}", slot, e),
169 };
170 runtime.block_on(async move {
171 let listener = bind(addr);
172 drop(bound);
173 match listener {
174 Ok(listener) => accept_loop(listener, stop).await,
175 Err(e) => eprintln!("per-core runtime {}: unable to listen on {}: {}", slot, addr, e),
176 }
177 });
178 });
179 match spawned {
180 Ok(h) => handles.push(h),
181 Err(e) => eprintln!("per-core runtime {}: {}", slot, e),
182 }
183 }
184 drop(bound_tx);
187 let _ = bound_rx.recv_timeout(std::time::Duration::from_secs(5));
188 handles
189}
190
191#[doc(hidden)]
193pub async fn stopped(stop: &mut watch::Receiver<bool>) {
194 let _ = stop.wait_for(|s| *s).await;
195}
196
197#[cfg(test)]
198mod tests {
199
200 #[test]
201 fn pkcs11_keys_are_spotted_in_config_text() {
202 use super::mentions_pkcs11_key;
203 assert!(mentions_pkcs11_key("tls_key_file = \"pkcs11:token=rws;object=k?module-path=/x.so\""));
204 assert!(mentions_pkcs11_key("[[virtual_host]]\ndomain = \"a\"\nkey_file = 'pkcs11:object=k'"));
205 assert!(!mentions_pkcs11_key("tls_key_file = \"/etc/rws/key.pem\""));
206 assert!(!mentions_pkcs11_key("comment = \"pkcs11:object=k\""));
207 assert!(!mentions_pkcs11_key(""));
208 }
209
210 use super::*;
211
212 #[cfg(unix)]
213 #[test]
214 fn the_port_accepts_connections_when_spawn_returns() {
215 let addr = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap();
216 let (stop_tx, stop_rx) = watch::channel(false);
217 let handles = spawn(addr, 2, stop_rx, |listener: tokio::net::TcpListener, mut stop: watch::Receiver<bool>| async move {
218 tokio::select! {
219 _ = async { while listener.accept().await.is_ok() {} } => {}
220 _ = stopped(&mut stop) => {}
221 }
222 });
223 assert_eq!(2, handles.len());
224 std::net::TcpStream::connect(addr).expect("listening as soon as spawn returns");
225 stop_tx.send(true).unwrap();
226 for handle in handles {
227 handle.join().unwrap();
228 }
229 }
230
231 #[test]
232 fn pool_key_is_shared_off_the_per_core_runtimes() {
233 assert!(matches!(pool_key("backend:80"), Cow::Borrowed("backend:80")));
234 }
235
236 #[test]
237 fn pool_key_is_per_runtime_on_a_per_core_thread() {
238 let keys = std::thread::spawn(|| {
239 SLOT.with(|s| s.set(3));
240 pool_key("backend:80").into_owned()
241 })
242 .join()
243 .unwrap();
244 assert_eq!("backend:80#3", keys);
245 }
246
247 #[test]
248 fn enabled_only_for_per_core() {
249 let _g = crate::test_env::lock();
250 let before = std::env::var(RWS_CONFIG_RUNTIME).ok();
251 std::env::remove_var(RWS_CONFIG_RUNTIME);
252 assert!(!enabled());
253 std::env::set_var(RWS_CONFIG_RUNTIME, "shared");
254 assert!(!enabled());
255 std::env::set_var(RWS_CONFIG_RUNTIME, " Per-Core ");
256 assert_eq!(cfg!(unix), enabled());
257 match before {
258 Some(v) => std::env::set_var(RWS_CONFIG_RUNTIME, v),
259 None => std::env::remove_var(RWS_CONFIG_RUNTIME),
260 }
261 }
262
263 #[test]
264 fn the_binary_defaults_to_per_core_unless_configured() {
265 let _g = crate::test_env::lock();
266 let before = std::env::var(RWS_CONFIG_RUNTIME).ok();
267 std::env::remove_var(RWS_CONFIG_RUNTIME);
268 default_to_per_core();
269 assert_eq!(Some("per-core".to_string()), std::env::var(RWS_CONFIG_RUNTIME).ok());
270 std::env::set_var(RWS_CONFIG_RUNTIME, "shared");
271 default_to_per_core();
272 assert_eq!(Some("shared".to_string()), std::env::var(RWS_CONFIG_RUNTIME).ok());
273 match before {
274 Some(v) => std::env::set_var(RWS_CONFIG_RUNTIME, v),
275 None => std::env::remove_var(RWS_CONFIG_RUNTIME),
276 }
277 }
278
279 #[test]
280 fn runtime_count_follows_tokio_worker_threads() {
281 let _g = crate::test_env::lock();
282 let before = std::env::var("TOKIO_WORKER_THREADS").ok();
283 std::env::set_var("TOKIO_WORKER_THREADS", "3");
284 assert_eq!(3, runtime_count());
285 std::env::set_var("TOKIO_WORKER_THREADS", "zero");
286 assert!(runtime_count() >= 1);
287 match before {
288 Some(v) => std::env::set_var("TOKIO_WORKER_THREADS", v),
289 None => std::env::remove_var("TOKIO_WORKER_THREADS"),
290 }
291 }
292
293 #[cfg(unix)]
296 #[test]
297 fn runtimes_share_the_port_and_stop() {
298 use std::io::{Read, Write};
299 use tokio::io::AsyncWriteExt;
300
301 let addr = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap();
303 let (stop_tx, stop_rx) = watch::channel(false);
304 let accept_loop = |listener: tokio::net::TcpListener, mut stop: watch::Receiver<bool>| async move {
305 loop {
306 tokio::select! {
307 result = listener.accept() => {
308 if let Ok((mut stream, _)) = result {
309 let slot = SLOT.with(|s| s.get());
310 let _ = stream.write_all(slot.to_string().as_bytes()).await;
311 }
312 }
313 _ = stopped(&mut stop) => break,
314 }
315 }
316 };
317 let handles = spawn(addr, 2, stop_rx, accept_loop);
318 assert_eq!(2, handles.len());
319
320 let mut answered = 0;
321 for _ in 0..50 {
322 let Ok(mut c) = std::net::TcpStream::connect(addr) else {
323 std::thread::sleep(std::time::Duration::from_millis(20)); continue;
325 };
326 let _ = c.write_all(b"x");
327 let mut slot = String::new();
328 let _ = c.read_to_string(&mut slot);
329 if !slot.is_empty() {
330 assert!(slot == "1" || slot == "2", "slot {slot}");
331 answered += 1;
332 }
333 }
334 assert!(answered > 0);
335
336 stop_tx.send(true).unwrap();
337 for h in handles {
338 h.join().unwrap();
339 }
340 assert!(std::net::TcpStream::connect(addr).is_err());
341 }
342
343 #[cfg(unix)]
344 #[test]
345 fn bind_reports_an_address_in_use_without_reuseport() {
346 let taken = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
347 let addr = taken.local_addr().unwrap();
348 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
349 runtime.block_on(async { assert!(bind(addr).is_err()) });
350 }
351}