1use core::time::Duration;
4
5use metrics_exporter_prometheus::BuildError;
6use tokio::net::TcpListener;
7
8#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum ServerError {
12 #[error(transparent)]
14 Listen(#[from] std::io::Error),
15
16 #[error("failed to initialize Prometheus metrics recorder: {0}")]
18 MetricsInit(#[source] BuildError),
19
20 #[error("failed to build tokio runtime: {0}")]
22 RuntimeBuild(#[source] std::io::Error),
23}
24
25pub struct ServerConfig {
27 pub host: String,
30 pub port: u16,
32}
33
34impl ServerConfig {
35 #[inline]
37 #[must_use]
38 pub fn new<Host>(host: Host, port: u16) -> Self
39 where
40 Host: Into<String>,
41 {
42 Self {
43 host: host.into(),
44 port,
45 }
46 }
47}
48
49#[inline]
63pub async fn bind(config: &ServerConfig) -> std::io::Result<TcpListener> {
64 TcpListener::bind((config.host.as_str(), config.port)).await
65}
66
67#[inline]
80pub async fn serve(config: ServerConfig) -> Result<(), ServerError> {
81 let listener = bind(&config).await?;
82 let bound_addr = listener.local_addr()?;
83
84 let handle = crate::metrics::install_global().map_err(ServerError::MetricsInit)?;
85
86 #[cfg(feature = "posthog")]
87 init_posthog().await;
88
89 let upkeep_task = spawn_upkeep(handle.clone());
90
91 tracing::info!(addr = %bound_addr, "docspec-http listening");
92
93 let server_result = axum::serve(listener, crate::router::router_with_metrics(handle))
94 .with_graceful_shutdown(shutdown_signal())
95 .await;
96
97 crate::telemetry::shutdown().await;
98 #[cfg(feature = "posthog")]
99 tracing::info!("posthog flush complete");
100
101 teardown_upkeep(upkeep_task).await;
102 server_result?;
103
104 Ok(())
105}
106
107#[allow(clippy::single_call_fn)]
109#[inline]
110fn spawn_upkeep(
111 handle: metrics_exporter_prometheus::PrometheusHandle,
112) -> tokio::task::JoinHandle<()> {
113 tokio::spawn(async move {
114 let mut interval = tokio::time::interval(Duration::from_secs(5));
115 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
116 loop {
117 interval.tick().await;
118 handle.run_upkeep();
119 }
120 })
121}
122
123#[allow(clippy::single_call_fn)]
125#[inline]
126async fn teardown_upkeep(task: tokio::task::JoinHandle<()>) {
127 task.abort();
128 match task.await {
129 Err(error) if error.is_cancelled() => {}
130 Err(error) => tracing::error!(%error, "metrics upkeep task failed during shutdown"),
131 Ok(()) => {}
132 }
133}
134
135#[cfg(feature = "posthog")]
143#[allow(clippy::single_call_fn)]
144#[inline]
145async fn init_posthog() {
146 let client = crate::telemetry::init_posthog_client_from_env().await;
147 crate::telemetry::install_posthog_client(client);
148}
149
150#[allow(clippy::single_call_fn)]
154#[inline]
155async fn shutdown_signal() {
156 use core::future;
157
158 use tokio::signal;
159
160 let ctrl_c = async {
161 if let Err(error) = signal::ctrl_c().await {
162 tracing::error!(%error, "failed to install Ctrl+C handler");
163 future::pending::<()>().await;
164 }
165 };
166
167 #[cfg(unix)]
168 let terminate = async {
169 match signal::unix::signal(signal::unix::SignalKind::terminate()) {
170 Ok(mut stream) => {
171 stream.recv().await;
172 }
173 Err(error) => {
174 tracing::error!(%error, "failed to install SIGTERM handler");
175 future::pending::<()>().await;
176 }
177 }
178 };
179
180 #[cfg(not(unix))]
181 let terminate = future::pending::<()>();
182
183 tokio::select! {
184 () = ctrl_c => {
185 tracing::info!("shutdown signal received, draining");
186 },
187 () = terminate => {
188 tracing::info!("shutdown signal received, draining");
189 },
190 }
191}