use super::{logging, shutdown};
use crate::{app_module, configs::Config};
use std::{future::IntoFuture, sync::Arc};
use tokio::sync::Notify;
pub async fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
crate::configs::load_env()?;
let config = Config::from_file()?;
super::static_files::validate(&config.static_files)?;
let _logging = logging::init(&config)?;
let container = Arc::new(app_module::AppModule::container()?);
let app = app_module::AppModule::router_with_container(container)?;
let operations = app_module::AppModule::api_operations();
let versions = super::versions::groups(&config, app.clone(), operations.clone())?;
let app = versions.mount(app)
.merge(super::docs::router(&config, operations)?)
.merge(super::docs::versioned_router(&config, &versions)?);
let app = super::router(app, &config);
let listener = tokio::net::TcpListener::bind(config.bind_address).await?;
let address = listener.local_addr()?;
let url = local_url(address);
tracing::info!(address = %address, url = %url, "server listening");
if config.docs.enabled {
tracing::info!(url = %format!("{url}/docs/"), "Swagger UI available");
tracing::info!(url = %format!("{url}/docs/openapi.json"), "OpenAPI document available");
for version in versions.versions() {
tracing::info!(api_version = version.name(), url = %format!("{url}/docs/{}/", version.name()), "Versioned Swagger UI available");
tracing::info!(api_version = version.name(), url = %format!("{url}/docs/{}/openapi.json", version.name()), "Versioned OpenAPI document available");
}
} else {
tracing::info!("Swagger documentation disabled (docs.enabled=false)");
}
if config.static_files.enabled {
tracing::info!(url = %format!("{url}{}", config.static_files.mount), "static files available");
}
let draining = Arc::new(Notify::new());
let shutdown_started = draining.clone();
let server = axum::serve(
listener,
app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(async move {
shutdown::signal().await;
tracing::info!("shutdown requested; draining requests");
shutdown_started.notify_one();
})
.into_future();
tokio::pin!(server);
tokio::select! {
result = &mut server => result?,
_ = draining.notified() => {
match tokio::time::timeout(config.shutdown_grace, &mut server).await {
Ok(result) => result?,
Err(error) => {
tracing::error!("shutdown drain deadline exceeded");
return Err(error.into());
}
}
}
}
tracing::info!("server stopped");
Ok(())
}
fn local_url(mut address: std::net::SocketAddr) -> String {
if address.ip().is_unspecified() {
address.set_ip(if address.is_ipv4() {
std::net::Ipv4Addr::LOCALHOST.into()
} else {
std::net::Ipv6Addr::LOCALHOST.into()
});
}
format!("http://{address}")
}
#[cfg(test)]
mod tests {
#[test]
fn startup_urls_use_accessible_hosts_and_preserve_ports() {
for (address, expected) in [
("0.0.0.0:3000", "http://127.0.0.1:3000"),
("127.0.0.1:4100", "http://127.0.0.1:4100"),
("[::]:3000", "http://[::1]:3000"),
("[::1]:4100", "http://[::1]:4100"),
] {
assert_eq!(super::local_url(address.parse().unwrap()), expected);
}
}
}