use std::time::{Duration, Instant};
use liburing::{CompletionDispatcher, Proactor, UringError};
use super::HttpClient;
use crate::{
Error, Result,
inbox::InboxCommand,
pool::{operation_id, stage_cancel},
};
impl HttpClient {
pub fn shutdown(&mut self, proactor: &mut Proactor) {
if let Err(error) = self.shutdown_with(proactor, &mut crate::dispatch::RejectForeign) {
panic!("HTTP client shutdown failed: {error}");
}
}
pub fn shutdown_with<D: CompletionDispatcher<Kind> + ?Sized, Kind>(
&mut self,
proactor: &mut Proactor,
peer: &mut D,
) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(5);
self.shutting_down = true;
self.resolver.begin_shutdown(proactor)?;
let requests: Vec<_> = self.requests.keys().copied().collect();
for id in requests {
let _cancelled = self.abort(proactor, id, Error::Cancelled);
}
self.intents.clear();
loop {
if Instant::now() >= deadline {
return Err(Error::Uring(UringError::Wait(libc::ETIMEDOUT)));
}
match self.drive_with(proactor, peer) {
Ok(_) => {}
Err(Error::Uring(UringError::SqRingFull)) => {
proactor.submit()?;
continue;
}
Err(error) => return Err(error),
}
if self.conns.is_empty()
&& self.timers.is_empty()
&& self.inbox.is_empty()
&& !self.resolver.has_pending_io()
{
return Ok(());
}
}
}
pub(super) fn flush_shutdown(&mut self, proactor: &mut Proactor) -> Result<()> {
while let Some(command) = self.inbox.pop() {
if let InboxCommand::Submit(command) = command {
self.deliver(
command.request_id,
Some(command.callback),
Error::ConnectionClosed,
);
}
}
self.intents.clear();
let ids: Vec<_> = self.conns.keys().copied().collect();
for id in ids {
self.start_close(proactor, id)?;
}
for &token in self.timers.keys() {
if !self.cancelling.contains(&token) {
let target = operation_id(self.config.net_subsystem, token)?;
stage_cancel(proactor, self.config.net_subsystem, target)?;
let _inserted = self.cancelling.insert(token);
}
}
self.resolver.flush(proactor)
}
fn start_close(&mut self, proactor: &mut Proactor, id: u64) -> Result<()> {
if let Some(conn) = self.conns.get_mut(&id) {
conn.want_close = true;
if conn.close_op.is_none() {
unsafe { libc::shutdown(conn.fd, libc::SHUT_RDWR) };
}
}
self.flush_connection(proactor, id)
}
}
impl Drop for HttpClient {
fn drop(&mut self) {
for (_, entry) in self.timers.drain() {
std::mem::forget(entry._timeout);
}
}
}
#[cfg(test)]
mod tests {
use std::{
cell::Cell,
io::{Read, Write},
net::TcpListener,
rc::Rc,
time::{Duration, Instant},
};
use liburing::{Proactor, ProactorConfig};
use nexora_proto::Request;
use crate::{ClientConfig, Error, HttpClient, Result};
fn exchange_then_shutdown(respond: bool) -> Result<()> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let address = listener.local_addr()?;
let peer = std::thread::spawn(move || -> std::io::Result<()> {
let (mut socket, _) = listener.accept()?;
socket.set_read_timeout(Some(Duration::from_secs(3)))?;
let mut request = Vec::new();
let mut byte = [0u8];
while !request.ends_with(b"\r\n\r\n") {
socket.read_exact(&mut byte)?;
request.push(byte[0]);
}
if respond {
socket.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")?;
}
assert_eq!(socket.read(&mut byte)?, 0, "client must send EOF");
Ok(())
});
let mut ring = Proactor::new(ProactorConfig::default())?;
let mut client = HttpClient::new(&mut ring, ClientConfig::default())?;
let calls = Rc::new(Cell::new(0));
let observed = Rc::clone(&calls);
let request = Request::get(&format!("http://{address}/"))?;
client.submit(&mut ring, request, move |_, _, result| {
if respond {
assert!(result.is_ok());
} else {
assert!(matches!(result, Err(Error::Cancelled)));
}
observed.set(observed.get() + 1);
})?;
let deadline = Instant::now() + Duration::from_secs(3);
loop {
client.drive(&mut ring)?;
let ready = if respond {
calls.get() == 1
} else {
client.conns.values().any(|conn| {
conn.rx_op.is_some()
&& conn.send_op.is_none()
&& conn.tx_pending.is_empty()
&& conn.tx_inflight.is_empty()
})
};
if ready {
break;
}
assert!(Instant::now() < deadline, "request did not reach the peer");
}
let descriptors: Vec<_> = client.conns.values().map(|conn| conn.fd).collect();
assert!(!descriptors.is_empty());
assert!(client.conns.values().any(|conn| conn.rx_op.is_some()));
let timer_fired = Rc::new(Cell::new(false));
let fired = Rc::clone(&timer_fired);
client.set_timer(&mut ring, Duration::from_secs(60), move |_, _| {
fired.set(true)
})?;
client.shutdown(&mut ring);
assert!(client.conns.is_empty());
assert!(client.timers.is_empty());
assert!(client.ops.is_empty());
assert_eq!(client.active_request_count(), 0);
assert_eq!(calls.get(), 1, "terminal callback must fire exactly once");
assert!(!timer_fired.get());
for fd in descriptors {
assert_eq!(unsafe { libc::fcntl(fd, libc::F_GETFD) }, -1);
}
client.shutdown(&mut ring);
assert_eq!(calls.get(), 1);
assert!(matches!(
client.submit(
&mut ring,
Request::get(&format!("http://{address}/"))?,
|_, _, _| {
panic!("rejected submission must not run its callback");
}
),
Err(Error::ConnectionClosed)
));
peer.join()
.map_err(|_| Error::InvalidState("peer thread panicked".into()))??;
ring.shutdown()?;
Ok(())
}
#[test]
fn lifecycle_shutdown_retires_a_keepalive_receive_and_closes_its_descriptor() -> Result<()> {
exchange_then_shutdown(true)
}
#[test]
fn lifecycle_shutdown_cancels_an_active_request_and_drains_timers() -> Result<()> {
exchange_then_shutdown(false)
}
}