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() {
45 if std::env::var_os(RWS_CONFIG_RUNTIME).is_none() {
46 std::env::set_var(RWS_CONFIG_RUNTIME, "per-core");
47 crate::server_config::invalidate();
48 }
49}
50
51pub fn runtime_count() -> usize {
54 std::env::var("TOKIO_WORKER_THREADS")
55 .ok()
56 .and_then(|v| v.trim().parse::<usize>().ok())
57 .filter(|n| *n > 0)
58 .unwrap_or_else(|| std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1))
59}
60
61pub(crate) fn pool_key(key: &str) -> Cow<'_, str> {
65 match SLOT.with(|s| s.get()) {
66 0 => Cow::Borrowed(key),
67 slot => {
69 let slot = crate::ext::decimal::Decimal::new(slot as u128);
70 let mut owned = String::with_capacity(key.len() + 1 + slot.as_str().len());
71 owned.push_str(key);
72 owned.push('#');
73 owned.push_str(slot.as_str());
74 Cow::Owned(owned)
75 }
76 }
77}
78
79#[cfg(unix)]
82pub(crate) fn bind(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
83 if let Some(listener) = crate::systemd::tcp(addr, None) {
86 listener.set_nonblocking(true)?;
87 return tokio::net::TcpListener::from_std(listener);
88 }
89 let socket = if addr.is_ipv4() { tokio::net::TcpSocket::new_v4()? } else { tokio::net::TcpSocket::new_v6()? };
90 socket.set_reuseaddr(true)?;
91 socket.set_reuseport(true)?;
92 socket.bind(addr)?;
93 socket.listen(1024)
94}
95
96#[cfg(unix)]
104pub(crate) fn spawn<F, Fut>(
105 addr: SocketAddr,
106 count: usize,
107 stop: watch::Receiver<bool>,
108 accept_loop: F,
109) -> Vec<std::thread::JoinHandle<()>>
110where
111 F: Fn(tokio::net::TcpListener, watch::Receiver<bool>) -> Fut + Send + Clone + 'static,
112 Fut: std::future::Future<Output = ()>,
113{
114 let mut handles = Vec::with_capacity(count);
115 let (bound_tx, bound_rx) = std::sync::mpsc::channel::<()>();
116 for slot in 1..=count {
117 let bound = bound_tx.clone();
118 let stop = stop.clone();
119 let accept_loop = accept_loop.clone();
120 let spawned = std::thread::Builder::new().name(format!("rws-core-{}", slot)).spawn(move || {
121 SLOT.with(|s| s.set(slot));
122 let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
123 Ok(r) => r,
124 Err(e) => return eprintln!("per-core runtime {}: {}", slot, e),
125 };
126 runtime.block_on(async move {
127 let listener = bind(addr);
128 drop(bound);
129 match listener {
130 Ok(listener) => accept_loop(listener, stop).await,
131 Err(e) => eprintln!("per-core runtime {}: unable to listen on {}: {}", slot, addr, e),
132 }
133 });
134 });
135 match spawned {
136 Ok(h) => handles.push(h),
137 Err(e) => eprintln!("per-core runtime {}: {}", slot, e),
138 }
139 }
140 drop(bound_tx);
143 let _ = bound_rx.recv_timeout(std::time::Duration::from_secs(5));
144 handles
145}
146
147pub(crate) async fn stopped(stop: &mut watch::Receiver<bool>) {
149 let _ = stop.wait_for(|s| *s).await;
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[cfg(unix)]
157 #[test]
158 fn the_port_accepts_connections_when_spawn_returns() {
159 let addr = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap();
160 let (stop_tx, stop_rx) = watch::channel(false);
161 let handles = spawn(addr, 2, stop_rx, |listener: tokio::net::TcpListener, mut stop: watch::Receiver<bool>| async move {
162 tokio::select! {
163 _ = async { while listener.accept().await.is_ok() {} } => {}
164 _ = stopped(&mut stop) => {}
165 }
166 });
167 assert_eq!(2, handles.len());
168 std::net::TcpStream::connect(addr).expect("listening as soon as spawn returns");
169 stop_tx.send(true).unwrap();
170 for handle in handles {
171 handle.join().unwrap();
172 }
173 }
174
175 #[test]
176 fn pool_key_is_shared_off_the_per_core_runtimes() {
177 assert!(matches!(pool_key("backend:80"), Cow::Borrowed("backend:80")));
178 }
179
180 #[test]
181 fn pool_key_is_per_runtime_on_a_per_core_thread() {
182 let keys = std::thread::spawn(|| {
183 SLOT.with(|s| s.set(3));
184 pool_key("backend:80").into_owned()
185 })
186 .join()
187 .unwrap();
188 assert_eq!("backend:80#3", keys);
189 }
190
191 #[test]
192 fn enabled_only_for_per_core() {
193 let _g = crate::test_env::lock();
194 let before = std::env::var(RWS_CONFIG_RUNTIME).ok();
195 std::env::remove_var(RWS_CONFIG_RUNTIME);
196 assert!(!enabled());
197 std::env::set_var(RWS_CONFIG_RUNTIME, "shared");
198 assert!(!enabled());
199 std::env::set_var(RWS_CONFIG_RUNTIME, " Per-Core ");
200 assert_eq!(cfg!(unix), enabled());
201 match before {
202 Some(v) => std::env::set_var(RWS_CONFIG_RUNTIME, v),
203 None => std::env::remove_var(RWS_CONFIG_RUNTIME),
204 }
205 }
206
207 #[test]
208 fn the_binary_defaults_to_per_core_unless_configured() {
209 let _g = crate::test_env::lock();
210 let before = std::env::var(RWS_CONFIG_RUNTIME).ok();
211 std::env::remove_var(RWS_CONFIG_RUNTIME);
212 default_to_per_core();
213 assert_eq!(Some("per-core".to_string()), std::env::var(RWS_CONFIG_RUNTIME).ok());
214 std::env::set_var(RWS_CONFIG_RUNTIME, "shared");
215 default_to_per_core();
216 assert_eq!(Some("shared".to_string()), std::env::var(RWS_CONFIG_RUNTIME).ok());
217 match before {
218 Some(v) => std::env::set_var(RWS_CONFIG_RUNTIME, v),
219 None => std::env::remove_var(RWS_CONFIG_RUNTIME),
220 }
221 }
222
223 #[test]
224 fn runtime_count_follows_tokio_worker_threads() {
225 let _g = crate::test_env::lock();
226 let before = std::env::var("TOKIO_WORKER_THREADS").ok();
227 std::env::set_var("TOKIO_WORKER_THREADS", "3");
228 assert_eq!(3, runtime_count());
229 std::env::set_var("TOKIO_WORKER_THREADS", "zero");
230 assert!(runtime_count() >= 1);
231 match before {
232 Some(v) => std::env::set_var("TOKIO_WORKER_THREADS", v),
233 None => std::env::remove_var("TOKIO_WORKER_THREADS"),
234 }
235 }
236
237 #[cfg(unix)]
240 #[test]
241 fn runtimes_share_the_port_and_stop() {
242 use std::io::{Read, Write};
243 use tokio::io::AsyncWriteExt;
244
245 let addr = std::net::TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap();
247 let (stop_tx, stop_rx) = watch::channel(false);
248 let accept_loop = |listener: tokio::net::TcpListener, mut stop: watch::Receiver<bool>| async move {
249 loop {
250 tokio::select! {
251 result = listener.accept() => {
252 if let Ok((mut stream, _)) = result {
253 let slot = SLOT.with(|s| s.get());
254 let _ = stream.write_all(slot.to_string().as_bytes()).await;
255 }
256 }
257 _ = stopped(&mut stop) => break,
258 }
259 }
260 };
261 let handles = spawn(addr, 2, stop_rx, accept_loop);
262 assert_eq!(2, handles.len());
263
264 let mut answered = 0;
265 for _ in 0..50 {
266 let Ok(mut c) = std::net::TcpStream::connect(addr) else {
267 std::thread::sleep(std::time::Duration::from_millis(20)); continue;
269 };
270 let _ = c.write_all(b"x");
271 let mut slot = String::new();
272 let _ = c.read_to_string(&mut slot);
273 if !slot.is_empty() {
274 assert!(slot == "1" || slot == "2", "slot {slot}");
275 answered += 1;
276 }
277 }
278 assert!(answered > 0);
279
280 stop_tx.send(true).unwrap();
281 for h in handles {
282 h.join().unwrap();
283 }
284 assert!(std::net::TcpStream::connect(addr).is_err());
285 }
286
287 #[cfg(unix)]
288 #[test]
289 fn bind_reports_an_address_in_use_without_reuseport() {
290 let taken = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
291 let addr = taken.local_addr().unwrap();
292 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
293 runtime.block_on(async { assert!(bind(addr).is_err()) });
294 }
295}