1use core::time::Duration;
4
5use metrics_exporter_prometheus::BuildError;
6use tokio::net::TcpListener;
7
8#[derive(Debug, thiserror::Error)]
10pub enum ServerError {
11 #[error(transparent)]
13 Listen(#[from] std::io::Error),
14
15 #[error("failed to initialize Prometheus metrics recorder: {0}")]
17 MetricsInit(#[source] BuildError),
18}
19
20pub struct ServerConfig {
22 pub host: String,
25 pub port: u16,
27}
28
29impl ServerConfig {
30 #[inline]
32 #[must_use]
33 pub fn new<Host>(host: Host, port: u16) -> Self
34 where
35 Host: Into<String>,
36 {
37 Self {
38 host: host.into(),
39 port,
40 }
41 }
42}
43
44#[inline]
58pub async fn bind(config: &ServerConfig) -> std::io::Result<TcpListener> {
59 TcpListener::bind((config.host.as_str(), config.port)).await
60}
61
62#[inline]
75pub async fn serve(config: ServerConfig) -> Result<(), ServerError> {
76 let listener = bind(&config).await?;
77 let bound_addr = listener.local_addr()?;
78
79 let handle = crate::metrics::install_global().map_err(ServerError::MetricsInit)?;
80 let upkeep_handle = handle.clone();
81 let upkeep_task = tokio::spawn(async move {
82 let mut interval = tokio::time::interval(Duration::from_secs(5));
83 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
84 loop {
85 interval.tick().await;
86 upkeep_handle.run_upkeep();
87 }
88 });
89
90 tracing::info!(addr = %bound_addr, "docspec-http listening");
91
92 let server_result = axum::serve(listener, crate::router::router_with_metrics(handle))
93 .with_graceful_shutdown(shutdown_signal())
94 .await;
95
96 upkeep_task.abort();
97 match upkeep_task.await {
98 Err(error) if error.is_cancelled() => {}
99 Err(error) => tracing::error!(%error, "metrics upkeep task failed during shutdown"),
100 Ok(()) => {}
101 }
102 server_result?;
103
104 Ok(())
105}
106
107#[allow(clippy::single_call_fn)]
111#[inline]
112async fn shutdown_signal() {
113 use core::future;
114
115 use tokio::signal;
116
117 let ctrl_c = async {
118 if let Err(error) = signal::ctrl_c().await {
119 tracing::error!(%error, "failed to install Ctrl+C handler");
120 future::pending::<()>().await;
121 }
122 };
123
124 #[cfg(unix)]
125 let terminate = async {
126 match signal::unix::signal(signal::unix::SignalKind::terminate()) {
127 Ok(mut stream) => {
128 stream.recv().await;
129 }
130 Err(error) => {
131 tracing::error!(%error, "failed to install SIGTERM handler");
132 future::pending::<()>().await;
133 }
134 }
135 };
136
137 #[cfg(not(unix))]
138 let terminate = future::pending::<()>();
139
140 tokio::select! {
141 () = ctrl_c => {
142 tracing::info!("shutdown signal received, draining");
143 },
144 () = terminate => {
145 tracing::info!("shutdown signal received, draining");
146 },
147 }
148}