use std::{
collections::HashMap,
error::Error,
fmt::Display,
io::{Error as IoError, Result as IoResult},
mem,
sync::Arc,
thread::{self, JoinHandle},
};
use axum::Router;
use futures::{FutureExt as _, channel::oneshot};
use crate::{
future::block_on,
providers::ProvidesExitCode,
types::{
hotswap_state::HotswapState,
http::{SocketAddr, SocketListener},
},
};
#[derive(Debug)]
pub struct HotReloadingAxumService<S: Send + Sync + 'static> {
state: HotswapState<S>,
router: Router<()>,
active_routers: HashMap<SocketAddr, (oneshot::Sender<()>, JoinHandle<IoResult<()>>)>,
}
impl<S: Send + Sync + 'static> HotReloadingAxumService<S> {
pub fn new(state: S, router: impl Fn(HotswapState<S>) -> Router<HotswapState<S>>) -> Self {
let parent_state = HotswapState::new(state);
let router = router(parent_state.clone_as_parent()).with_state(parent_state.clone_as_parent());
Self {
state: parent_state,
router,
active_routers: HashMap::new(),
}
}
pub fn replace_state(&self, state: S) -> Arc<S> {
self.state.replace_parent_state(state).expect("must be a state parent")
}
pub fn has_dead_threads(&self) -> bool {
self.active_routers
.iter()
.any(|(_, (signal, thread))| thread.is_finished() || signal.is_canceled())
}
pub fn restart_dead_threads(&mut self) -> Result<(), HotReloadAxumError> {
if !self.has_dead_threads() {
return Ok(());
}
let sockets: Vec<SocketAddr> = self.active_routers.keys().cloned().collect();
self.bind_sockets(sockets)
}
pub fn stop(&mut self) {
for (socket, (signal, thread)) in mem::take(&mut self.active_routers) {
let _ = signal.send(());
Self::join_thread_and_log_error(&socket, thread, true);
}
}
pub fn bind_sockets(&mut self, sockets: impl IntoIterator<Item = SocketAddr>) -> Result<(), HotReloadAxumError> {
let mut previous_routers = mem::take(&mut self.active_routers);
for socket in sockets.into_iter() {
if previous_routers
.get(&socket)
.is_none_or(|(signal, thread)| thread.is_finished() || signal.is_canceled())
{
if let Some((_, dead_thread)) = previous_routers.remove(&socket) {
Self::join_thread_and_log_error(&socket, dead_thread, false);
}
let (bind_error_send, bind_error_recv) = oneshot::channel::<IoError>();
let (shutdown_send, shutdown_recv) = oneshot::channel::<()>();
let router = self.router.clone();
let l_socket = socket.clone();
match thread::Builder::new().name("hot-reload axum".into()).spawn(move || {
match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.and_then(|runtime| {
let listener = runtime.block_on(SocketListener::bind(l_socket))?;
Ok((runtime, listener))
}) {
Ok((runtime, listener)) => {
tracing::debug!("axum thread successfully bound to {}", listener.original_addr());
drop(bind_error_send);
runtime.block_on(
axum::serve(listener, router)
.with_graceful_shutdown(shutdown_recv.map(|_| {}))
.into_future(),
)
},
Err(runtime_err) => {
let _ = bind_error_send.send(runtime_err);
Ok(())
},
}
}) {
Ok(new_thread) => {
match block_on(bind_error_recv) {
Ok(bind_err) => {
tracing::error!("Unable to bind to {socket}: {bind_err}");
Self::join_thread_and_log_error(&socket, new_thread, true);
},
Err(oneshot::Canceled) => {
self.active_routers.insert(socket, (shutdown_send, new_thread));
},
}
},
Err(err) => {
tracing::error!("failed to spawn axum thread serving {socket}: {err}");
},
}
} else if let Some(active_router) = previous_routers.remove(&socket) {
self.active_routers.insert(socket, active_router);
}
}
for (socket, (signal, thread)) in previous_routers {
let _ = signal.send(());
Self::join_thread_and_log_error(&socket, thread, true);
}
if self.active_routers.is_empty() {
Err(HotReloadAxumError {})
} else {
Ok(())
}
}
fn join_thread_and_log_error(socket: &SocketAddr, thread: JoinHandle<IoResult<()>>, expected: bool) {
match thread.join() {
Ok(Ok(())) => {
if expected {
tracing::info!("axum thread serving {socket} has finished");
} else {
tracing::error!("axum thread serving {socket} finished unexpectedly");
}
},
Ok(Err(axum_err)) => {
tracing::error!("axum instance serving {socket} encountered an irrecoverable error: {axum_err}");
},
Err(panic) => {
if let Some(panic_str) = panic.downcast_ref::<&str>() {
tracing::error!("axum thread serving {socket} panicked with message: {panic_str}");
} else if let Some(panic_string) = panic.downcast_ref::<String>() {
tracing::error!("axum thread serving {socket} panicked with message: {panic_string}");
} else {
tracing::error!("axum thread serving {socket} panicked with unknown error type");
}
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HotReloadAxumError {}
impl Display for HotReloadAxumError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("no sockets could be listened to")
}
}
impl Error for HotReloadAxumError {}
impl ProvidesExitCode for HotReloadAxumError {
fn exit_code(&self) -> std::process::ExitCode {
crate::app::consts::EX_CONFIG.into()
}
}
#[cfg(test)]
#[path = "../tests/app/axum.rs"]
mod tests;