use std::{future::Future, net::SocketAddr};
use axum::Router;
use tokio::net::TcpListener;
#[cfg(feature = "queue")]
use crate::{Error, queue::Queue};
use crate::{Result, config::Server};
#[cfg(feature = "queue")]
use std::sync::Arc;
#[cfg(feature = "queue")]
use tokio::sync::watch;
pub async fn shutdown_signal() {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("install Ctrl-C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
() = ctrl_c => {},
() = terminate => {},
}
}
pub async fn serve(router: Router, config: &Server) -> Result<()> {
bind_and_serve(router, config, shutdown_signal()).await
}
pub async fn serve_until(
router: Router,
listener: TcpListener,
shutdown: impl Future<Output = ()> + Send + 'static,
) -> Result<()> {
axum::serve(
listener,
router.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown)
.await?;
Ok(())
}
async fn bind_and_serve(
router: Router,
config: &Server,
stop: impl Future<Output = ()> + Send + 'static,
) -> Result<()> {
let address = format!("{}:{}", config.binding, config.port);
let listener = TcpListener::bind(&address).await?;
tracing::info!(address, "listening");
serve_until(router, listener, stop).await
}
#[cfg(feature = "queue")]
pub async fn run(router: Option<Router>, queue: Option<Arc<Queue>>, config: &Server) -> Result<()> {
run_until(router, queue, config, shutdown_signal()).await
}
#[cfg(feature = "queue")]
async fn run_until(
router: Option<Router>,
queue: Option<Arc<Queue>>,
config: &Server,
stop: impl Future<Output = ()> + Send + 'static,
) -> Result<()> {
let modes = match (&router, &queue) {
(Some(_), Some(_)) => "server, worker",
(Some(_), None) => "server",
(None, Some(_)) => "worker",
(None, None) => return Err(Error::string("neither a server nor a worker to run")),
};
tracing::info!("modes: {modes}");
let (stop_jobs, stopped) = watch::channel(false);
let jobs = queue.map(|queue| tokio::spawn(queue.run(stopped)));
let served = if let Some(router) = router {
bind_and_serve(router, config, stop).await
} else {
stop.await;
Ok(())
};
let _ = stop_jobs.send(true);
if let Some(jobs) = jobs {
jobs.await
.map_err(|error| Error::string(&format!("job loops failed: {error}")))?;
}
served
}
#[cfg(test)]
mod tests {
use super::*;
use axum::routing::get;
use std::time::Duration;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::oneshot,
};
#[tokio::test]
async fn shutdown_lets_a_request_in_flight_finish() {
let (started_tx, started_rx) = oneshot::channel::<()>();
let started = std::sync::Arc::new(std::sync::Mutex::new(Some(started_tx)));
let app = Router::new().route(
"/slow",
get(move || {
let started = started.lock().unwrap().take();
async move {
if let Some(started) = started {
let _ = started.send(());
}
tokio::time::sleep(Duration::from_millis(100)).await;
"finished"
}
}),
);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let (stop_tx, stop_rx) = oneshot::channel::<()>();
let server = tokio::spawn(serve_until(app, listener, async {
let _ = stop_rx.await;
}));
let mut stream = TcpStream::connect(address).await.unwrap();
stream
.write_all(b"GET /slow HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n")
.await
.unwrap();
started_rx.await.unwrap();
stop_tx.send(()).unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).await.unwrap();
assert!(response.contains("200 OK"), "{response}");
assert!(response.ends_with("finished"), "{response}");
server.await.unwrap().unwrap();
}
#[tokio::test]
async fn serve_reports_an_address_it_cannot_bind() {
let config = Server {
port: 1,
binding: "256.0.0.1".to_string(),
host: String::new(),
};
assert!(serve(Router::new(), &config).await.is_err());
}
#[cfg(feature = "queue")]
fn local() -> Server {
Server {
port: 0,
binding: "127.0.0.1".to_string(),
host: String::new(),
}
}
#[cfg(feature = "queue")]
#[tokio::test]
async fn nothing_to_run_is_an_error() {
assert!(run_until(None, None, &local(), async {}).await.is_err());
}
#[cfg(feature = "queue")]
#[tokio::test]
async fn a_server_alone_stops_when_told() {
run_until(Some(Router::new()), None, &local(), async {})
.await
.unwrap();
}
#[cfg(feature = "queue")]
async fn queue() -> Arc<Queue> {
let uri = std::env::var("AXA_TEST_DATABASE_URL").expect("AXA_TEST_DATABASE_URL");
let options = crate::queue::QueueOptions::default();
Arc::new(Queue::connect(&uri, options).await.unwrap())
}
#[cfg(feature = "queue")]
#[tokio::test]
#[ignore = "needs AXA_TEST_DATABASE_URL"]
async fn a_worker_alone_stops_when_told() {
run_until(None, Some(queue().await), &local(), async {})
.await
.unwrap();
}
#[cfg(feature = "queue")]
#[tokio::test]
#[ignore = "needs AXA_TEST_DATABASE_URL"]
async fn the_job_loops_stop_when_the_server_cannot_start() {
let config = Server {
port: 1,
binding: "256.0.0.1".to_string(),
host: String::new(),
};
let router = Some(Router::new());
let run = run_until(router, Some(queue().await), &config, std::future::pending());
let outcome = tokio::time::timeout(std::time::Duration::from_secs(10), run).await;
assert!(outcome.expect("the job loops kept running").is_err());
}
}