1use std::io::Write;
2use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
3use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4use std::sync::{Arc, Mutex, OnceLock};
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 lys_root: Arc<OnceLock<crate::auth_lys::root_presence_start::RootSlot>>,
50 local_addr: SocketAddr,
51 interrupt_target: SocketAddr,
54 shutdown: Arc<AtomicBool>,
55 active_stream: Arc<Mutex<Option<TcpStream>>>,
63 unloadable: SharedUnloadableConversations,
69 reissue: SharedOperatorCredentialReissue,
74 reclaim: super::reclaim_route::SharedReclaimSpine,
77 ephemeral_sweep: Arc<OnceLock<liminal::durability::EphemeralSweepCounts>>,
78 listen_addresses: Arc<OnceLock<Vec<SocketAddr>>>,
79 link_bridges: crate::link_bridge::BridgeRegistry,
80 worker: Option<JoinHandle<Result<(), ServerError>>>,
81 #[cfg(test)]
86 accept_attempts: Arc<AtomicU64>,
87 #[cfg(test)]
90 shed_count: Arc<AtomicU64>,
91 #[cfg(test)]
96 requests_entered: Arc<AtomicU64>,
97}
98
99impl HealthServerHandle {
100 pub fn install_grant_stream(
105 &self,
106 state: Arc<crate::auth_membership::grant_stream::StreamState>,
107 ) -> Result<(), ServerError> {
108 self.grant_stream
109 .set(state)
110 .map_err(|_| ServerError::HealthEndpoint {
111 message: "grant_stream_owner_already_installed".into(),
112 })
113 }
114 pub fn install_lys_root(
120 &self,
121 root: crate::auth_lys::root_presence_start::RootSlot,
122 ) -> Result<(), ServerError> {
123 self.lys_root
124 .set(root)
125 .map_err(|_| ServerError::HealthEndpoint {
126 message: "lys_root_already_installed".into(),
127 })
128 }
129
130 pub fn install_listen_addresses(&self, addresses: Vec<SocketAddr>) -> Result<(), ServerError> {
135 self.listen_addresses
136 .set(addresses)
137 .map_err(|_| ServerError::HealthEndpoint {
138 message: "wire listen addresses are already installed".to_owned(),
139 })
140 }
141
142 #[must_use]
144 pub fn link_bridges(&self) -> crate::link_bridge::BridgeRegistry {
145 self.link_bridges.clone()
146 }
147 #[must_use]
149 pub const fn local_addr(&self) -> SocketAddr {
150 self.local_addr
151 }
152
153 pub fn install_spine(&self, spine: &Arc<haematite::EventStore>) {
167 if !self.reclaim.install(spine) {
168 tracing::warn!(
169 "the spine was installed on the health endpoint already; the first stands"
170 );
171 }
172 }
173
174 pub fn install_unloadable_record(&self, record: UnloadableConversationRecord) {
175 self.unloadable.install(record);
176 }
177
178 pub fn install_credential_reissuer(&self, reissuer: Arc<dyn OperatorCredentialReissuer>) {
188 self.reissue.install(reissuer);
189 }
190
191 pub fn install_ephemeral_sweep_report(
196 &self,
197 report: liminal::durability::EphemeralSweepCounts,
198 ) -> Result<(), ServerError> {
199 self.ephemeral_sweep
200 .set(report)
201 .map_err(|_| ServerError::HealthEndpoint {
202 message: "ephemeral sweep report is already installed".to_owned(),
203 })
204 }
205
206 pub fn shutdown(mut self) -> Result<(), ServerError> {
213 self.stop_worker()
214 }
215
216 fn stop_worker(&mut self) -> Result<(), ServerError> {
217 self.shutdown.store(true, Ordering::SeqCst);
218 let Some(worker) = self.worker.take() else {
219 return Ok(());
220 };
221 let in_flight = self
232 .active_stream
233 .lock()
234 .map_or(None, |mut slot| slot.take());
235 if let Some(stream) = in_flight
236 && let Err(error) = stream.shutdown(Shutdown::Both)
237 {
238 tracing::debug!(%error, "health endpoint in-flight stream already closed at shutdown");
239 }
240 match TcpStream::connect(self.interrupt_target) {
250 Ok(waker) => drop(waker),
251 Err(error) => tracing::warn!(
252 interrupt_target = %self.interrupt_target,
253 %error,
254 "health endpoint accept interrupt could not connect; the accept \
255 worker will only stop if it has already exited"
256 ),
257 }
258
259 worker.join().map_err(|_| ServerError::HealthEndpoint {
260 message: "health endpoint worker thread terminated unexpectedly".to_owned(),
261 })?
262 }
263
264 #[cfg(test)]
266 fn accept_attempts(&self) -> u64 {
267 self.accept_attempts.load(Ordering::SeqCst)
268 }
269
270 #[cfg(test)]
272 fn shed_count(&self) -> u64 {
273 self.shed_count.load(Ordering::SeqCst)
274 }
275
276 #[cfg(test)]
278 fn requests_entered(&self) -> u64 {
279 self.requests_entered.load(Ordering::SeqCst)
280 }
281}
282
283impl Drop for HealthServerHandle {
284 fn drop(&mut self) {
285 if let Err(error) = self.stop_worker() {
286 tracing::debug!(%error, "health endpoint shutdown during drop failed");
287 }
288 }
289}
290
291pub fn start_health_server(
303 bind_address: SocketAddr,
304 readiness: SharedReadinessState,
305) -> Result<HealthServerHandle, ServerError> {
306 let started_at = std::time::SystemTime::now()
307 .duration_since(std::time::UNIX_EPOCH)
308 .map_err(|error| ServerError::HealthEndpoint {
309 message: format!("startup clock: {error}"),
310 })?;
311 let started_at =
312 u64::try_from(started_at.as_millis()).map_err(|error| ServerError::HealthEndpoint {
313 message: format!("startup clock: {error}"),
314 })?;
315 let listener =
316 TcpListener::bind(bind_address).map_err(|error| ServerError::HealthEndpoint {
317 message: format!("failed to bind health endpoint at {bind_address}: {error}"),
318 })?;
319 let local_addr = listener
323 .local_addr()
324 .map_err(|error| ServerError::HealthEndpoint {
325 message: format!("failed to inspect health endpoint listener address: {error}"),
326 })?;
327 let interrupt_target = loopback_interrupt_target(local_addr);
328 let shutdown = Arc::new(AtomicBool::new(false));
329 let active_stream = Arc::new(Mutex::new(None));
330 let unloadable = SharedUnloadableConversations::default();
331 let reissue = SharedOperatorCredentialReissue::default();
332 let reclaim = super::reclaim_route::SharedReclaimSpine::default();
333 let ephemeral_sweep = Arc::new(OnceLock::new());
334 let listen_addresses = Arc::new(OnceLock::new());
335 let link_bridges = crate::link_bridge::BridgeRegistry::default();
336 let grant_stream = Arc::new(OnceLock::new());
337 let lys_root = Arc::new(OnceLock::new());
338 let drafts = Arc::new(Mutex::new(None));
339 let accept_attempts = Arc::new(AtomicU64::new(0));
340 let shed_count = Arc::new(AtomicU64::new(0));
341 let requests_entered = Arc::new(AtomicU64::new(0));
342 let worker_shutdown = Arc::clone(&shutdown);
343 let worker_stream = Arc::clone(&active_stream);
344 let worker_attempts = Arc::clone(&accept_attempts);
345 let worker_shed = Arc::clone(&shed_count);
346 let worker_entered = Arc::clone(&requests_entered);
347 let served = ServedState {
348 started_at,
349 drafts: drafts.clone(),
350 grant_stream: Arc::clone(&grant_stream),
351 lys_root: Arc::clone(&lys_root),
352 readiness,
353 unloadable: unloadable.clone(),
354 reissue: reissue.clone(),
355 reclaim: reclaim.clone(),
356 ephemeral_sweep: Arc::clone(&ephemeral_sweep),
357 listen_addresses: Arc::clone(&listen_addresses),
358 link_bridges: link_bridges.clone(),
359 };
360 let worker = thread::spawn(move || {
361 serve(
362 &listener,
363 &served,
364 &worker_shutdown,
365 &worker_stream,
366 &worker_attempts,
367 &worker_shed,
368 &worker_entered,
369 )
370 });
371
372 Ok(HealthServerHandle {
373 drafts,
374 grant_stream,
375 lys_root,
376 local_addr,
377 interrupt_target,
378 shutdown,
379 active_stream,
380 unloadable,
381 reissue,
382 reclaim,
383 ephemeral_sweep,
384 listen_addresses,
385 link_bridges,
386 worker: Some(worker),
387 #[cfg(test)]
388 accept_attempts,
389 #[cfg(test)]
390 shed_count,
391 #[cfg(test)]
392 requests_entered,
393 })
394}
395
396#[derive(Debug, Clone)]
399struct ServedState {
400 started_at: u64,
401 drafts: admin::DraftRoute,
402 grant_stream: Arc<OnceLock<Arc<crate::auth_membership::grant_stream::StreamState>>>,
403 lys_root: Arc<OnceLock<crate::auth_lys::root_presence_start::RootSlot>>,
404 readiness: SharedReadinessState,
405 unloadable: SharedUnloadableConversations,
406 reissue: SharedOperatorCredentialReissue,
407 reclaim: super::reclaim_route::SharedReclaimSpine,
408 ephemeral_sweep: Arc<OnceLock<liminal::durability::EphemeralSweepCounts>>,
409 listen_addresses: Arc<OnceLock<Vec<SocketAddr>>>,
410 link_bridges: crate::link_bridge::BridgeRegistry,
411}
412
413#[derive(serde::Serialize)]
414struct HealthWithLinks {
415 started_at: u64,
416 grant_stream: Option<crate::auth_membership::grant_stream::StreamHealth>,
417 lys_root: Option<&'static str>,
419 #[serde(skip_serializing_if = "Option::is_none")]
420 lys_root_cause: Option<&'static str>,
421 #[serde(flatten, serialize_with = "health_with_lys_asks")]
422 health: super::checks::HealthStatus,
423 links: std::collections::BTreeMap<String, crate::link_bridge::BridgeCounters>,
424 listen_addresses: Vec<SocketAddr>,
425}
426
427fn health_with_lys_asks<S: serde::Serializer>(
428 health: &super::checks::HealthStatus,
429 serializer: S,
430) -> Result<S::Ok, S::Error> {
431 #[derive(serde::Serialize)]
434 struct WithAsks<'a> {
435 #[serde(flatten)]
436 health: &'a super::checks::HealthStatus,
437 lys_ask: crate::auth_lys::live::AskStatistics,
438 }
439 let lys_ask = crate::auth_lys::live::ask_statistics().map_err(serde::ser::Error::custom)?;
440 serde::Serialize::serialize(&WithAsks { health, lys_ask }, serializer)
441}
442
443#[derive(serde::Serialize)]
444struct ReadyWithStream {
445 ready: bool,
446 unmet_conditions: Vec<ReadyCondition>,
447 grant_stream: Option<crate::auth_membership::grant_stream::StreamHealth>,
448}
449
450#[derive(serde::Serialize)]
451#[serde(untagged)]
452enum ReadyCondition {
453 Existing(super::checks::ReadinessCondition),
454 Stream(&'static str),
455}
456
457mod responses;
458mod routes;
459mod serve;
460use responses::{StatusCode, empty_response, json_response, parse_request_line, response};
461use routes::response_for_request;
462use serve::serve;
463
464#[cfg(test)]
465#[path = "endpoint/tests.rs"]
466mod tests;
467
468#[cfg(test)]
469mod lys_ask_health_tests {
470 use super::*;
471
472 #[test]
473 fn health_reports_lys_ask_count_and_running_maximum() -> Result<(), Box<dyn std::error::Error>>
474 {
475 let health = HealthWithLinks {
476 started_at: 1,
477 grant_stream: None,
478 lys_root: None,
479 lys_root_cause: None,
480 health: health_check(),
481 links: std::collections::BTreeMap::new(),
482 listen_addresses: Vec::new(),
483 };
484 let value = serde_json::to_value(health)?;
485 assert!(value["lys_ask"]["count"].is_u64());
486 assert!(value["lys_ask"]["max_elapsed_ms"].is_f64());
487 assert_eq!(value["status"], "healthy");
488 assert!(value["grant_stream"].is_null());
489 Ok(())
490 }
491}
492
493pub(crate) mod admin;