How can a TcpListener
be closed, so that "blocked" accept()
operations are aborted?
I have multi-threaded HTTP server that I want to go down "gracefully" when SIGNIT (ctrl+c) is received.
Here is what I tried:
impl Server {
pub fn new(addr: IpAddr, port: u16) -> Self {
Self { addr, port }
}
pub fn run(self, handler: impl Handler + 'static) {
let socket_addr = SocketAddr::new(self.addr, self.port);
info!("Listening on {}", socket_addr);
let listener = TcpListener::bind(socket_addr).unwrap();
let stop = Arc::new(AtomicBool::new(false));
let mut handles = Vec::new();
let num_threads = 2usize * cpu_count();
for _n in 0usize..num_threads {
if let Ok(thread_listener) = listener.try_clone() {
let my_handler = handler.clone();
let my_stop = stop.clone();
handles.push(thread::spawn(move || {
let thread_id: u64 = thread::current().id().as_u64().into();
info!("Thread #{thread_id} is up!");
loop {
let connection = thread_listener.accept();
if !my_stop.load(Ordering::Acquire) {
match connection {
Ok((stream, addr)) => Self::process_request(thread_id, &my_handler, stream, addr),
Err(err) => error!("Failed to establish the connection: {}", err),
}
} else {
break; /*shutting down */
}
}
}));
}
}
drop(ctrlc::set_handler(move || {
warn!("Interrupted -> shutting down server !!!");
stop.store(true, Ordering::Release);
}));
for handle in handles.drain(..) {
drop(handle.join());
}
info!("Server is down! Goodbye...");
}
fn process_request(id: u64, handler: &impl Handler, mut stream: TcpStream, addr: SocketAddr) {
/* ... */
}
}
The problem is that the "worker" threads will be blocked in TcpListener ::accept()
, even after we have set the shutdown flag. Well, at least until more requests happen to come in. But that is far from reliable...
How can "main()" thread close the TcpListener
to unblock the threads ????
Thank you!
Note 1:
Yes, I could set the TcpListener
to non-blocking mode. But that would be very inefficient
Note 2:
I already tried to use cancellable_io
, which seemed great at a first glance.
Unfortunately, cancellable_io::TcpListener::try_clone()
always fails! It's unusable
socket already registered!
Note 3:
socket2
has a Socket::shutdown()
method. Looks like exactly what we need.
...but it does not interrupt the blocked accept()
call. Only the next accept()
would fail then...