use std::future::Future;
use std::time::Duration;
use tokio::sync::watch;
use tracing::warn;
use crate::signal::wait_for_shutdown;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DrainOutcome {
Clean,
Forced,
}
#[derive(Debug, thiserror::Error)]
pub enum ServeError {
#[error("serve I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("post-drain receipt flush failed: {0}")]
Flush(String),
}
pub async fn run_until_drained<S, D>(
server: S,
shutdown: watch::Receiver<bool>,
drain_timeout: Duration,
on_drained: D,
) -> Result<DrainOutcome, ServeError>
where
S: std::future::IntoFuture<Output = std::io::Result<()>>,
D: Future<Output = Result<(), String>>,
{
let mut server = Box::pin(server.into_future());
let signalled = wait_for_shutdown(shutdown);
let outcome = tokio::select! {
result = &mut server => match result {
Ok(()) => DrainOutcome::Clean,
Err(source) => return Err(ServeError::Io(source)),
},
() = signalled => {
match tokio::time::timeout(drain_timeout, &mut server).await {
Ok(Ok(())) => DrainOutcome::Clean,
Ok(Err(source)) => return Err(ServeError::Io(source)),
Err(_elapsed) => {
let drain_ms = u64::try_from(drain_timeout.as_millis()).unwrap_or(u64::MAX);
warn!(
drain_timeout_ms = drain_ms,
"drain deadline exceeded; force-closing remaining connections"
);
DrainOutcome::Forced
}
}
}
};
if matches!(outcome, DrainOutcome::Forced) {
drop(server);
}
on_drained.await.map_err(ServeError::Flush)?;
Ok(outcome)
}