1use std::io::Write;
2use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
3use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4use std::sync::{Arc, Mutex, OnceLock, PoisonError};
5use std::thread::{self, JoinHandle};
6use std::time::Duration;
7
8use crate::ServerError;
9use crate::server::listener::{
10 acquire_reserve_descriptor, loopback_interrupt_target, prepare_accepted_socket,
11 shed_on_fd_exhaustion,
12};
13
14use super::checks::{SharedReadinessState, health_check, readiness_check};
15use super::reissue::{
16 OperatorCredentialReissueOutcome, OperatorCredentialReissueRefusal,
17 OperatorCredentialReissueRequest, OperatorCredentialReissuer, SharedOperatorCredentialReissue,
18};
19use super::unloadable::{SharedUnloadableConversations, UnloadableConversationRecord};
20
21use super::metrics_route;
22
23const HEALTH_PATH: &str = "/health";
24const READY_PATH: &str = "/ready";
25const METRICS_PATH: &str = "/metrics";
26const UNLOADABLE_PATH: &str = "/unloadable-conversations";
27const REISSUE_PATH: &str = "/operator/credential-reissue";
29const RECLAIM_PATH: &str = "/reclaim";
31const QUIESCE_PATH: &str = "/operator/reclaim-quiesce";
33const APPLICATION_JSON: &str = "application/json";
34const READ_BUFFER_BYTES: usize = 2048;
35
36#[derive(Debug)]
46pub struct HealthServerHandle {
47 drafts: admin::DraftRoute,
48 grant_stream: Arc<OnceLock<Arc<crate::auth_membership::grant_stream::StreamState>>>,
49 local_addr: SocketAddr,
50 interrupt_target: SocketAddr,
53 shutdown: Arc<AtomicBool>,
54 active_stream: Arc<Mutex<Option<TcpStream>>>,
62 unloadable: SharedUnloadableConversations,
68 reissue: SharedOperatorCredentialReissue,
73 reclaim: super::reclaim_route::SharedReclaimSpine,
76 ephemeral_sweep: Arc<OnceLock<liminal::durability::EphemeralSweepCounts>>,
77 listen_addresses: Arc<OnceLock<Vec<SocketAddr>>>,
78 link_bridges: crate::link_bridge::BridgeRegistry,
79 worker: Option<JoinHandle<Result<(), ServerError>>>,
80 #[cfg(test)]
85 accept_attempts: Arc<AtomicU64>,
86 #[cfg(test)]
89 shed_count: Arc<AtomicU64>,
90 #[cfg(test)]
95 requests_entered: Arc<AtomicU64>,
96}
97
98impl HealthServerHandle {
99 pub fn install_grant_stream(
104 &self,
105 state: Arc<crate::auth_membership::grant_stream::StreamState>,
106 ) -> Result<(), ServerError> {
107 self.grant_stream
108 .set(state)
109 .map_err(|_| ServerError::HealthEndpoint {
110 message: "grant_stream_owner_already_installed".into(),
111 })
112 }
113 pub fn install_listen_addresses(&self, addresses: Vec<SocketAddr>) -> Result<(), ServerError> {
118 self.listen_addresses
119 .set(addresses)
120 .map_err(|_| ServerError::HealthEndpoint {
121 message: "wire listen addresses are already installed".to_owned(),
122 })
123 }
124
125 #[must_use]
127 pub fn link_bridges(&self) -> crate::link_bridge::BridgeRegistry {
128 self.link_bridges.clone()
129 }
130 #[must_use]
132 pub const fn local_addr(&self) -> SocketAddr {
133 self.local_addr
134 }
135
136 pub fn install_spine(&self, spine: &Arc<haematite::EventStore>) {
150 if !self.reclaim.install(spine) {
151 tracing::warn!(
152 "the spine was installed on the health endpoint already; the first stands"
153 );
154 }
155 }
156
157 pub fn install_unloadable_record(&self, record: UnloadableConversationRecord) {
158 self.unloadable.install(record);
159 }
160
161 pub fn install_credential_reissuer(&self, reissuer: Arc<dyn OperatorCredentialReissuer>) {
171 self.reissue.install(reissuer);
172 }
173
174 pub fn install_ephemeral_sweep_report(
179 &self,
180 report: liminal::durability::EphemeralSweepCounts,
181 ) -> Result<(), ServerError> {
182 self.ephemeral_sweep
183 .set(report)
184 .map_err(|_| ServerError::HealthEndpoint {
185 message: "ephemeral sweep report is already installed".to_owned(),
186 })
187 }
188
189 pub fn shutdown(mut self) -> Result<(), ServerError> {
196 self.stop_worker()
197 }
198
199 fn stop_worker(&mut self) -> Result<(), ServerError> {
200 self.shutdown.store(true, Ordering::SeqCst);
201 let Some(worker) = self.worker.take() else {
202 return Ok(());
203 };
204 let in_flight = self
213 .active_stream
214 .lock()
215 .unwrap_or_else(PoisonError::into_inner)
216 .take();
217 if let Some(stream) = in_flight {
218 let _ = stream.shutdown(Shutdown::Both);
219 }
220 match TcpStream::connect(self.interrupt_target) {
230 Ok(waker) => drop(waker),
231 Err(error) => tracing::warn!(
232 interrupt_target = %self.interrupt_target,
233 %error,
234 "health endpoint accept interrupt could not connect; the accept \
235 worker will only stop if it has already exited"
236 ),
237 }
238
239 worker.join().map_err(|_| ServerError::HealthEndpoint {
240 message: "health endpoint worker thread terminated unexpectedly".to_owned(),
241 })?
242 }
243
244 #[cfg(test)]
246 fn accept_attempts(&self) -> u64 {
247 self.accept_attempts.load(Ordering::SeqCst)
248 }
249
250 #[cfg(test)]
252 fn shed_count(&self) -> u64 {
253 self.shed_count.load(Ordering::SeqCst)
254 }
255
256 #[cfg(test)]
258 fn requests_entered(&self) -> u64 {
259 self.requests_entered.load(Ordering::SeqCst)
260 }
261}
262
263impl Drop for HealthServerHandle {
264 fn drop(&mut self) {
265 if let Err(error) = self.stop_worker() {
266 tracing::debug!(%error, "health endpoint shutdown during drop failed");
267 }
268 }
269}
270
271pub fn start_health_server(
283 bind_address: SocketAddr,
284 readiness: SharedReadinessState,
285) -> Result<HealthServerHandle, ServerError> {
286 let started_at = std::time::SystemTime::now()
287 .duration_since(std::time::UNIX_EPOCH)
288 .map_err(|error| ServerError::HealthEndpoint {
289 message: format!("startup clock: {error}"),
290 })?;
291 let started_at =
292 u64::try_from(started_at.as_millis()).map_err(|error| ServerError::HealthEndpoint {
293 message: format!("startup clock: {error}"),
294 })?;
295 let listener =
296 TcpListener::bind(bind_address).map_err(|error| ServerError::HealthEndpoint {
297 message: format!("failed to bind health endpoint at {bind_address}: {error}"),
298 })?;
299 let local_addr = listener
303 .local_addr()
304 .map_err(|error| ServerError::HealthEndpoint {
305 message: format!("failed to inspect health endpoint listener address: {error}"),
306 })?;
307 let interrupt_target = loopback_interrupt_target(local_addr);
308 let shutdown = Arc::new(AtomicBool::new(false));
309 let active_stream = Arc::new(Mutex::new(None));
310 let unloadable = SharedUnloadableConversations::default();
311 let reissue = SharedOperatorCredentialReissue::default();
312 let reclaim = super::reclaim_route::SharedReclaimSpine::default();
313 let ephemeral_sweep = Arc::new(OnceLock::new());
314 let listen_addresses = Arc::new(OnceLock::new());
315 let link_bridges = crate::link_bridge::BridgeRegistry::default();
316 let grant_stream = Arc::new(OnceLock::new());
317 let drafts = Arc::new(Mutex::new(None));
318 let accept_attempts = Arc::new(AtomicU64::new(0));
319 let shed_count = Arc::new(AtomicU64::new(0));
320 let requests_entered = Arc::new(AtomicU64::new(0));
321 let worker_shutdown = Arc::clone(&shutdown);
322 let worker_stream = Arc::clone(&active_stream);
323 let worker_attempts = Arc::clone(&accept_attempts);
324 let worker_shed = Arc::clone(&shed_count);
325 let worker_entered = Arc::clone(&requests_entered);
326 let served = ServedState {
327 started_at,
328 drafts: drafts.clone(),
329 grant_stream: Arc::clone(&grant_stream),
330 readiness,
331 unloadable: unloadable.clone(),
332 reissue: reissue.clone(),
333 reclaim: reclaim.clone(),
334 ephemeral_sweep: Arc::clone(&ephemeral_sweep),
335 listen_addresses: Arc::clone(&listen_addresses),
336 link_bridges: link_bridges.clone(),
337 };
338 let worker = thread::spawn(move || {
339 serve(
340 &listener,
341 &served,
342 &worker_shutdown,
343 &worker_stream,
344 &worker_attempts,
345 &worker_shed,
346 &worker_entered,
347 )
348 });
349
350 Ok(HealthServerHandle {
351 drafts,
352 grant_stream,
353 local_addr,
354 interrupt_target,
355 shutdown,
356 active_stream,
357 unloadable,
358 reissue,
359 reclaim,
360 ephemeral_sweep,
361 listen_addresses,
362 link_bridges,
363 worker: Some(worker),
364 #[cfg(test)]
365 accept_attempts,
366 #[cfg(test)]
367 shed_count,
368 #[cfg(test)]
369 requests_entered,
370 })
371}
372
373#[derive(Debug, Clone)]
376struct ServedState {
377 started_at: u64,
378 drafts: admin::DraftRoute,
379 grant_stream: Arc<OnceLock<Arc<crate::auth_membership::grant_stream::StreamState>>>,
380 readiness: SharedReadinessState,
381 unloadable: SharedUnloadableConversations,
382 reissue: SharedOperatorCredentialReissue,
383 reclaim: super::reclaim_route::SharedReclaimSpine,
384 ephemeral_sweep: Arc<OnceLock<liminal::durability::EphemeralSweepCounts>>,
385 listen_addresses: Arc<OnceLock<Vec<SocketAddr>>>,
386 link_bridges: crate::link_bridge::BridgeRegistry,
387}
388
389#[derive(serde::Serialize)]
390struct HealthWithLinks {
391 started_at: u64,
392 grant_stream: Option<crate::auth_membership::grant_stream::StreamHealth>,
393 #[serde(flatten, serialize_with = "health_with_lys_asks")]
394 health: super::checks::HealthStatus,
395 links: std::collections::BTreeMap<String, crate::link_bridge::BridgeCounters>,
396 listen_addresses: Vec<SocketAddr>,
397}
398
399fn health_with_lys_asks<S: serde::Serializer>(
400 health: &super::checks::HealthStatus,
401 serializer: S,
402) -> Result<S::Ok, S::Error> {
403 #[derive(serde::Serialize)]
406 struct WithAsks<'a> {
407 #[serde(flatten)]
408 health: &'a super::checks::HealthStatus,
409 lys_ask: crate::auth_lys::live::AskStatistics,
410 }
411 let lys_ask = crate::auth_lys::live::ask_statistics().map_err(serde::ser::Error::custom)?;
412 serde::Serialize::serialize(&WithAsks { health, lys_ask }, serializer)
413}
414
415#[derive(serde::Serialize)]
416struct ReadyWithStream {
417 ready: bool,
418 unmet_conditions: Vec<ReadyCondition>,
419 grant_stream: Option<crate::auth_membership::grant_stream::StreamHealth>,
420}
421
422#[derive(serde::Serialize)]
423#[serde(untagged)]
424enum ReadyCondition {
425 Existing(super::checks::ReadinessCondition),
426 Stream(&'static str),
427}
428
429mod responses;
430mod routes;
431mod serve;
432use responses::{StatusCode, empty_response, json_response, parse_request_line, response};
433use routes::response_for_request;
434use serve::serve;
435
436#[cfg(test)]
437#[path = "endpoint/tests.rs"]
438mod tests;
439
440#[cfg(test)]
441mod lys_ask_health_tests {
442 use super::*;
443
444 #[test]
445 fn health_reports_lys_ask_count_and_running_maximum() -> Result<(), Box<dyn std::error::Error>>
446 {
447 let health = HealthWithLinks {
448 started_at: 1,
449 grant_stream: None,
450 health: health_check(),
451 links: std::collections::BTreeMap::new(),
452 listen_addresses: Vec::new(),
453 };
454 let value = serde_json::to_value(health)?;
455 assert!(value["lys_ask"]["count"].is_u64());
456 assert!(value["lys_ask"]["max_elapsed_ms"].is_f64());
457 assert_eq!(value["status"], "healthy");
458 assert!(value["grant_stream"].is_null());
459 Ok(())
460 }
461}
462
463pub(crate) mod admin;