Skip to content

Commit 925bb86

Browse files
committed
feat(rpc): graceful-shutdown accept loop
Add RpcServer::serve_with_shutdown(Arc<AtomicBool>) which polls non-blocking accept on a 100 ms cadence so the loop observes shutdown without parking on an open socket. Accepted connections are restored to blocking mode before being handed to the per-connection worker so the configured idle_timeout still applies. A unit test exercises the shutdown path with a bound-to-port-0 listener. Op: extend
1 parent 0610529 commit 925bb86

1 file changed

Lines changed: 86 additions & 23 deletions

File tree

crates/rpc/src/server.rs

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::handlers::Handler;
1414

1515
const MAX_HEADER_BYTES: usize = 16 * 1_024;
1616
const MAX_BODY_BYTES: usize = 16 * 1_024 * 1_024;
17+
const POLL_INTERVAL: core::time::Duration = core::time::Duration::from_millis(100);
1718

1819
/// Synchronous HTTP/1.1 JSON-RPC server.
1920
pub struct RpcServer {
@@ -56,33 +57,67 @@ impl RpcServer {
5657
pub fn serve(self) -> io::Result<()> {
5758
let active = Arc::new(Mutex::new(0_usize));
5859
for stream in self.listener.incoming() {
59-
let mut stream = stream?;
60-
let should_accept = {
61-
let mut count = active.lock();
62-
if *count >= self.max_connections {
63-
false
64-
} else {
65-
*count += 1;
66-
true
60+
self.handle_accept(&active, stream?)?;
61+
}
62+
Ok(())
63+
}
64+
65+
/// Runs the accept loop until `shutdown` is set to `true`.
66+
///
67+
/// Polls non-blocking accept on a fixed cadence so the loop can observe
68+
/// shutdown without parking on an open socket. Each accepted connection
69+
/// is restored to blocking mode and handed to a bounded worker thread,
70+
/// preserving the configured `idle_timeout` per connection.
71+
#[allow(clippy::needless_pass_by_value)]
72+
pub fn serve_with_shutdown(
73+
self,
74+
shutdown: alloc::sync::Arc<core::sync::atomic::AtomicBool>,
75+
) -> io::Result<()> {
76+
use core::sync::atomic::Ordering;
77+
78+
self.listener.set_nonblocking(true)?;
79+
let active = Arc::new(Mutex::new(0_usize));
80+
while !shutdown.load(Ordering::Acquire) {
81+
match self.listener.accept() {
82+
Ok((stream, _addr)) => {
83+
stream.set_nonblocking(false)?;
84+
self.handle_accept(&active, stream)?;
6785
}
68-
};
69-
if !should_accept {
70-
write_status(&mut stream, 503, "Service Unavailable", b"busy", false)?;
71-
continue;
86+
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
87+
thread::sleep(POLL_INTERVAL);
88+
}
89+
Err(error) => return Err(error),
7290
}
91+
}
92+
Ok(())
93+
}
7394

74-
let auth = Arc::clone(&self.auth);
75-
let handler = Arc::clone(&self.handler);
76-
let active = Arc::clone(&active);
77-
let idle_timeout = self.idle_timeout;
78-
thread::spawn(move || {
79-
if let Err(error) = serve_connection(stream, &auth, &handler, idle_timeout) {
80-
debug!(%error, "rpc connection closed with error");
81-
}
82-
let mut count = active.lock();
83-
*count = count.saturating_sub(1);
84-
});
95+
fn handle_accept(&self, active: &Arc<Mutex<usize>>, mut stream: TcpStream) -> io::Result<()> {
96+
let should_accept = {
97+
let mut count = active.lock();
98+
if *count >= self.max_connections {
99+
false
100+
} else {
101+
*count += 1;
102+
true
103+
}
104+
};
105+
if !should_accept {
106+
write_status(&mut stream, 503, "Service Unavailable", b"busy", false)?;
107+
return Ok(());
85108
}
109+
110+
let auth = Arc::clone(&self.auth);
111+
let handler = Arc::clone(&self.handler);
112+
let active = Arc::clone(active);
113+
let idle_timeout = self.idle_timeout;
114+
thread::spawn(move || {
115+
if let Err(error) = serve_connection(stream, &auth, &handler, idle_timeout) {
116+
debug!(%error, "rpc connection closed with error");
117+
}
118+
let mut count = active.lock();
119+
*count = count.saturating_sub(1);
120+
});
86121
Ok(())
87122
}
88123
}
@@ -255,3 +290,31 @@ fn write_status(
255290
stream.write_all(body)?;
256291
stream.flush()
257292
}
293+
294+
#[cfg(test)]
295+
mod tests {
296+
use super::*;
297+
use core::sync::atomic::{AtomicBool, Ordering};
298+
299+
use crate::context::Context;
300+
301+
#[test]
302+
#[allow(clippy::expect_used)]
303+
fn serve_with_shutdown_exits_on_signal() -> std::io::Result<()> {
304+
let auth = Arc::new(Auth::basic("alice", "secret"));
305+
let handler = Arc::new(Handler::new(Arc::new(Context::new())));
306+
let server = RpcServer::bind(
307+
"127.0.0.1:0",
308+
auth,
309+
handler,
310+
4,
311+
core::time::Duration::from_millis(500),
312+
)?;
313+
let shutdown = Arc::new(AtomicBool::new(false));
314+
let shutdown_clone = Arc::clone(&shutdown);
315+
let handle = std::thread::spawn(move || server.serve_with_shutdown(shutdown_clone));
316+
std::thread::sleep(core::time::Duration::from_millis(150));
317+
shutdown.store(true, Ordering::Release);
318+
handle.join().expect("join serve thread")
319+
}
320+
}

0 commit comments

Comments
 (0)