use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
use crate::auth::{AuthConfig, AuthContext};
use crate::cli::error::CliError;
use crate::config::Config;
use crate::error::Error;
use crate::signing_key::SigningKey;
use crate::{
admin_router, create_report_router, did_document_router, spawn_writer, storage,
subscribe_router, wellknown_router,
};
const DRAIN_TIMEOUT: Duration = Duration::from_secs(30);
pub async fn run<F>(config: Config, shutdown: F) -> Result<(), CliError>
where
F: std::future::Future<Output = ()> + Send + 'static,
{
config
.validate()
.map_err(|e| CliError::Config(e.to_string()))?;
let key = SigningKey::load_from_file(&config.signing_key_path)?;
let pool = storage::open(&config.db_path)
.await
.map_err(|e| CliError::MigrationFailed(e.to_string()))?;
let writer = spawn_writer(pool.clone(), key, config.service_did.clone())
.await
.map_err(map_spawn_writer_error)?;
let auth = Arc::new(AuthContext::new(AuthConfig {
service_did: config.service_did.clone(),
..AuthConfig::default()
}));
let router = admin_router(
pool.clone(),
writer.clone(),
auth.clone(),
config.admin.clone().into(),
)
.merge(create_report_router(
pool.clone(),
auth.clone(),
crate::CreateReportConfig {
db_path: config.db_path.clone(),
..crate::CreateReportConfig::default()
},
))
.merge(subscribe_router(
pool.clone(),
writer.clone(),
crate::SubscribeConfig::default(),
))
.merge(wellknown_router())
.merge(did_document_router(pool.clone(), config.clone()));
let listener = TcpListener::bind(config.bind_addr)
.await
.map_err(|source| CliError::BindFailed {
addr: config.bind_addr,
source,
})?;
let local_addr = listener.local_addr().unwrap_or(config.bind_addr);
tracing::info!(
bind_addr = %local_addr,
service_did = %config.service_did,
"cairn listening; lease acquired"
);
let (drain_start_tx, drain_start_rx) = tokio::sync::oneshot::channel::<()>();
let shutdown_wrapper = async move {
shutdown.await;
let _ = drain_start_tx.send(());
};
let serve_fut = axum::serve(
listener,
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(shutdown_wrapper);
let drain_timer = async move {
match drain_start_rx.await {
Ok(()) => tokio::time::sleep(DRAIN_TIMEOUT).await,
Err(_) => std::future::pending::<()>().await,
}
};
enum Outcome {
Clean,
AxumError(std::io::Error),
DrainTimeout,
}
let outcome = tokio::select! {
res = serve_fut => match res {
Ok(()) => Outcome::Clean,
Err(e) => Outcome::AxumError(e),
},
_ = drain_timer => Outcome::DrainTimeout,
};
if let Err(e) = writer.shutdown().await {
tracing::warn!(error = %e, "writer shutdown failed during serve exit");
}
match outcome {
Outcome::Clean => Ok(()),
Outcome::AxumError(e) => Err(CliError::Startup(format!("axum serve error: {e}"))),
Outcome::DrainTimeout => {
tracing::warn!(
drain_timeout_secs = DRAIN_TIMEOUT.as_secs(),
"drain timeout exceeded after shutdown signal; forcing exit"
);
Ok(())
}
}
}
fn map_spawn_writer_error(e: Error) -> CliError {
match e {
Error::LeaseHeld {
instance_id,
age_secs,
} => CliError::LeaseConflict {
instance_id,
age_secs,
},
other => CliError::Startup(format!("writer spawn: {other}")),
}
}