1use std::io::{Read, Write};
2use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
3use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
4use std::sync::{Arc, Mutex, PoisonError};
5use std::thread::{self, JoinHandle};
6use std::time::Duration;
7
8use crate::ServerError;
9use crate::server::listener::{
10 loopback_interrupt_target, prepare_accepted_socket, shed_on_fd_exhaustion,
11};
12
13use super::checks::{SharedReadinessState, health_check, readiness_check};
14use super::reissue::{
15 OperatorCredentialReissueOutcome, OperatorCredentialReissueRefusal,
16 OperatorCredentialReissueRequest, OperatorCredentialReissuer, SharedOperatorCredentialReissue,
17};
18use super::unloadable::{SharedUnloadableConversations, UnloadableConversationRecord};
19
20use super::metrics_route;
21
22const HEALTH_PATH: &str = "/health";
23const READY_PATH: &str = "/ready";
24const METRICS_PATH: &str = "/metrics";
25const UNLOADABLE_PATH: &str = "/unloadable-conversations";
26const REISSUE_PATH: &str = "/operator/credential-reissue";
28const APPLICATION_JSON: &str = "application/json";
29const READ_BUFFER_BYTES: usize = 2048;
30
31#[derive(Debug)]
41pub struct HealthServerHandle {
42 local_addr: SocketAddr,
43 interrupt_target: SocketAddr,
46 shutdown: Arc<AtomicBool>,
47 active_stream: Arc<Mutex<Option<TcpStream>>>,
55 unloadable: SharedUnloadableConversations,
61 reissue: SharedOperatorCredentialReissue,
66 worker: Option<JoinHandle<Result<(), ServerError>>>,
67 #[cfg(test)]
72 accept_attempts: Arc<AtomicU64>,
73 #[cfg(test)]
76 shed_count: Arc<AtomicU64>,
77 #[cfg(test)]
82 requests_entered: Arc<AtomicU64>,
83}
84
85impl HealthServerHandle {
86 #[must_use]
88 pub const fn local_addr(&self) -> SocketAddr {
89 self.local_addr
90 }
91
92 pub fn install_unloadable_record(&self, record: UnloadableConversationRecord) {
105 self.unloadable.install(record);
106 }
107
108 pub fn install_credential_reissuer(&self, reissuer: Arc<dyn OperatorCredentialReissuer>) {
118 self.reissue.install(reissuer);
119 }
120
121 pub fn shutdown(mut self) -> Result<(), ServerError> {
128 self.stop_worker()
129 }
130
131 fn stop_worker(&mut self) -> Result<(), ServerError> {
132 self.shutdown.store(true, Ordering::SeqCst);
133 let Some(worker) = self.worker.take() else {
134 return Ok(());
135 };
136 let in_flight = self
145 .active_stream
146 .lock()
147 .unwrap_or_else(PoisonError::into_inner)
148 .take();
149 if let Some(stream) = in_flight {
150 let _ = stream.shutdown(Shutdown::Both);
151 }
152 if let Ok(waker) = TcpStream::connect(self.interrupt_target) {
160 drop(waker);
161 }
162
163 worker.join().map_err(|_| ServerError::HealthEndpoint {
164 message: "health endpoint worker thread terminated unexpectedly".to_owned(),
165 })?
166 }
167
168 #[cfg(test)]
170 fn accept_attempts(&self) -> u64 {
171 self.accept_attempts.load(Ordering::SeqCst)
172 }
173
174 #[cfg(test)]
176 fn shed_count(&self) -> u64 {
177 self.shed_count.load(Ordering::SeqCst)
178 }
179
180 #[cfg(test)]
182 fn requests_entered(&self) -> u64 {
183 self.requests_entered.load(Ordering::SeqCst)
184 }
185}
186
187impl Drop for HealthServerHandle {
188 fn drop(&mut self) {
189 if let Err(error) = self.stop_worker() {
190 tracing::debug!(%error, "health endpoint shutdown during drop failed");
191 }
192 }
193}
194
195pub fn start_health_server(
207 bind_address: SocketAddr,
208 readiness: SharedReadinessState,
209) -> Result<HealthServerHandle, ServerError> {
210 let listener =
211 TcpListener::bind(bind_address).map_err(|error| ServerError::HealthEndpoint {
212 message: format!("failed to bind health endpoint at {bind_address}: {error}"),
213 })?;
214 let local_addr = listener
218 .local_addr()
219 .map_err(|error| ServerError::HealthEndpoint {
220 message: format!("failed to inspect health endpoint listener address: {error}"),
221 })?;
222 let interrupt_target = loopback_interrupt_target(local_addr);
223 let shutdown = Arc::new(AtomicBool::new(false));
224 let active_stream = Arc::new(Mutex::new(None));
225 let unloadable = SharedUnloadableConversations::default();
226 let reissue = SharedOperatorCredentialReissue::default();
227 let accept_attempts = Arc::new(AtomicU64::new(0));
228 let shed_count = Arc::new(AtomicU64::new(0));
229 let requests_entered = Arc::new(AtomicU64::new(0));
230 let worker_shutdown = Arc::clone(&shutdown);
231 let worker_stream = Arc::clone(&active_stream);
232 let worker_attempts = Arc::clone(&accept_attempts);
233 let worker_shed = Arc::clone(&shed_count);
234 let worker_entered = Arc::clone(&requests_entered);
235 let served = ServedState {
236 readiness,
237 unloadable: unloadable.clone(),
238 reissue: reissue.clone(),
239 };
240 let worker = thread::spawn(move || {
241 serve(
242 &listener,
243 &served,
244 &worker_shutdown,
245 &worker_stream,
246 &worker_attempts,
247 &worker_shed,
248 &worker_entered,
249 )
250 });
251
252 Ok(HealthServerHandle {
253 local_addr,
254 interrupt_target,
255 shutdown,
256 active_stream,
257 unloadable,
258 reissue,
259 worker: Some(worker),
260 #[cfg(test)]
261 accept_attempts,
262 #[cfg(test)]
263 shed_count,
264 #[cfg(test)]
265 requests_entered,
266 })
267}
268
269#[derive(Debug, Clone)]
272struct ServedState {
273 readiness: SharedReadinessState,
274 unloadable: SharedUnloadableConversations,
275 reissue: SharedOperatorCredentialReissue,
276}
277
278fn serve(
279 listener: &TcpListener,
280 served: &ServedState,
281 shutdown: &AtomicBool,
282 active_stream: &Mutex<Option<TcpStream>>,
283 accept_attempts: &AtomicU64,
284 shed_count: &AtomicU64,
285 requests_entered: &AtomicU64,
286) -> Result<(), ServerError> {
287 let mut reserve = listener.try_clone().ok();
290 while !shutdown.load(Ordering::SeqCst) {
291 accept_attempts.fetch_add(1, Ordering::SeqCst);
292 match listener.accept() {
293 Ok((stream, ..)) => {
294 let admitted = {
303 let mut slot = active_stream.lock().unwrap_or_else(PoisonError::into_inner);
304 if shutdown.load(Ordering::SeqCst) {
305 false
306 } else {
307 *slot = stream.try_clone().ok();
308 true
309 }
310 };
311 if !admitted {
312 drop(stream);
313 continue;
314 }
315 prepare_accepted_socket(&stream, None);
319 requests_entered.fetch_add(1, Ordering::SeqCst);
320 let result = handle_connection(stream, served);
326 *active_stream.lock().unwrap_or_else(PoisonError::into_inner) = None;
330 if let Err(error) = result {
331 tracing::debug!(%error, "health endpoint connection error");
332 }
333 }
334 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
335 Err(error) if is_transient_accept_error(&error) => {
336 shed_on_fd_exhaustion(listener, &mut reserve, shed_count, &error);
337 }
338 Err(error) => {
339 return Err(ServerError::HealthEndpoint {
340 message: format!("health endpoint accept failed: {error}"),
341 });
342 }
343 }
344 }
345
346 Ok(())
347}
348
349fn is_transient_accept_error(error: &std::io::Error) -> bool {
352 matches!(error.raw_os_error(), Some(code) if code == 24 || code == 23)
353}
354
355fn handle_connection(mut stream: TcpStream, served: &ServedState) -> Result<(), ServerError> {
356 stream
357 .set_nonblocking(false)
358 .map_err(|error| ServerError::HealthEndpoint {
359 message: format!("failed to configure health request stream: {error}"),
360 })?;
361 stream
362 .set_read_timeout(Some(Duration::from_secs(2)))
363 .map_err(|error| ServerError::HealthEndpoint {
364 message: format!("failed to set health request read timeout: {error}"),
365 })?;
366
367 let mut buffer = [0_u8; READ_BUFFER_BYTES];
368 let bytes_read = stream
369 .read(&mut buffer)
370 .map_err(|error| ServerError::HealthEndpoint {
371 message: format!("failed to read health request: {error}"),
372 })?;
373
374 if bytes_read == 0 {
375 return Ok(());
376 }
377
378 let response = response_for_request(&buffer[..bytes_read], served)?;
379 stream
380 .write_all(&response)
381 .map_err(|error| ServerError::HealthEndpoint {
382 message: format!("failed to write health response: {error}"),
383 })?;
384 stream.flush().map_err(|error| ServerError::HealthEndpoint {
385 message: format!("failed to flush health response: {error}"),
386 })
387}
388
389fn response_for_request(request: &[u8], served: &ServedState) -> Result<Vec<u8>, ServerError> {
390 let Ok(request) = std::str::from_utf8(request) else {
391 return Ok(empty_response(StatusCode::BadRequest));
392 };
393 let Some((method, path)) = parse_request_line(request) else {
394 return Ok(empty_response(StatusCode::BadRequest));
395 };
396
397 match (method, path) {
398 ("GET", HEALTH_PATH) => json_response(StatusCode::Ok, &health_check()),
399 ("GET", READY_PATH) => {
400 let status = readiness_check(&served.readiness.snapshot());
401 let status_code = if status.ready {
402 StatusCode::Ok
403 } else {
404 StatusCode::ServiceUnavailable
405 };
406 json_response(status_code, &status)
407 }
408 ("GET", METRICS_PATH) => Ok(response(
409 StatusCode::Ok,
410 Some(metrics_route::CONTENT_TYPE),
411 metrics_route::render_body().as_bytes(),
412 )),
413 ("GET", UNLOADABLE_PATH) => json_response(StatusCode::Ok, &served.unloadable.status()),
417 (_, HEALTH_PATH | READY_PATH | METRICS_PATH | UNLOADABLE_PATH) => {
418 Ok(empty_response(StatusCode::MethodNotAllowed))
419 }
420 _ if path.split('?').next() == Some(REISSUE_PATH) => {
425 credential_reissue_response(method, path, served)
426 }
427 _ => Ok(empty_response(StatusCode::NotFound)),
428 }
429}
430
431fn credential_reissue_response(
445 method: &str,
446 path: &str,
447 served: &ServedState,
448) -> Result<Vec<u8>, ServerError> {
449 if method != "POST" {
450 return Ok(empty_response(StatusCode::MethodNotAllowed));
451 }
452 let query = path.split_once('?').map_or("", |(_, query)| query);
453 let Some(request) = parse_reissue_query(query) else {
454 return Ok(empty_response(StatusCode::BadRequest));
455 };
456 match served.reissue.reissue(request) {
457 Ok(None) => Ok(empty_response(StatusCode::ServiceUnavailable)),
460 Ok(Some(OperatorCredentialReissueOutcome::Issued(issued))) => {
461 json_response(StatusCode::Ok, &issued)
462 }
463 Ok(Some(OperatorCredentialReissueOutcome::Refused(refusal))) => {
464 json_response(reissue_refusal_status(&refusal), &refusal)
465 }
466 Err(error) => {
467 tracing::error!(%error, "operator credential re-issue could not be decided");
470 json_response(
471 StatusCode::ServiceUnavailable,
472 &serde_json::json!({ "error": error.message }),
473 )
474 }
475 }
476}
477
478const fn reissue_refusal_status(refusal: &OperatorCredentialReissueRefusal) -> StatusCode {
485 match refusal {
486 OperatorCredentialReissueRefusal::ConversationUnknown { .. }
487 | OperatorCredentialReissueRefusal::ParticipantUnknown { .. } => StatusCode::NotFound,
488 OperatorCredentialReissueRefusal::Retired { .. }
489 | OperatorCredentialReissueRefusal::LiveBinding { .. }
490 | OperatorCredentialReissueRefusal::DetachReplayOpen { .. }
491 | OperatorCredentialReissueRefusal::LiveReceipt { .. }
492 | OperatorCredentialReissueRefusal::GenerationMismatch { .. } => StatusCode::Conflict,
493 }
494}
495
496fn parse_reissue_query(query: &str) -> Option<OperatorCredentialReissueRequest> {
503 let mut conversation_id = None;
504 let mut participant_id = None;
505 let mut expected_current_generation = None;
506 for pair in query.split('&') {
507 let (name, value) = pair.split_once('=')?;
508 let value = value.parse::<u64>().ok()?;
509 let slot = match name {
510 "conversation_id" => &mut conversation_id,
511 "participant_id" => &mut participant_id,
512 "expected_current_generation" => &mut expected_current_generation,
513 _ => return None,
514 };
515 if slot.replace(value).is_some() {
516 return None;
517 }
518 }
519 Some(OperatorCredentialReissueRequest {
520 conversation_id: conversation_id?,
521 participant_id: participant_id?,
522 expected_current_generation: expected_current_generation?,
523 })
524}
525
526fn parse_request_line(request: &str) -> Option<(&str, &str)> {
527 let request_line = request.lines().next()?;
528 let mut parts = request_line.split_whitespace();
529 let method = parts.next()?;
530 let path = parts.next()?;
531 parts.next()?;
532
533 Some((method, path))
534}
535
536fn json_response<T>(status: StatusCode, value: &T) -> Result<Vec<u8>, ServerError>
537where
538 T: serde::Serialize,
539{
540 let body = serde_json::to_vec(value).map_err(|error| ServerError::HealthEndpoint {
541 message: format!("failed to serialize health response: {error}"),
542 })?;
543 Ok(response(status, Some(APPLICATION_JSON), &body))
544}
545
546fn empty_response(status: StatusCode) -> Vec<u8> {
547 response(status, None, &[])
548}
549
550fn response(status: StatusCode, content_type: Option<&str>, body: &[u8]) -> Vec<u8> {
551 let mut response = Vec::new();
552 let status_line = format!("HTTP/1.1 {} {}\r\n", status.code(), status.reason());
553 response.extend_from_slice(status_line.as_bytes());
554 response.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
555 response.extend_from_slice(b"Connection: close\r\n");
556 if let Some(content_type) = content_type {
557 response.extend_from_slice(format!("Content-Type: {content_type}\r\n").as_bytes());
558 }
559 response.extend_from_slice(b"\r\n");
560 response.extend_from_slice(body);
561 response
562}
563
564#[derive(Debug, Clone, Copy, PartialEq, Eq)]
565enum StatusCode {
566 Ok,
567 BadRequest,
568 NotFound,
569 MethodNotAllowed,
570 Conflict,
571 ServiceUnavailable,
572}
573
574impl StatusCode {
575 const fn code(self) -> u16 {
576 match self {
577 Self::Ok => 200,
578 Self::BadRequest => 400,
579 Self::NotFound => 404,
580 Self::MethodNotAllowed => 405,
581 Self::Conflict => 409,
582 Self::ServiceUnavailable => 503,
583 }
584 }
585
586 const fn reason(self) -> &'static str {
587 match self {
588 Self::Ok => "OK",
589 Self::BadRequest => "Bad Request",
590 Self::NotFound => "Not Found",
591 Self::MethodNotAllowed => "Method Not Allowed",
592 Self::Conflict => "Conflict",
593 Self::ServiceUnavailable => "Service Unavailable",
594 }
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use std::io::{Read, Write};
601 use std::net::{SocketAddr, TcpStream};
602 use std::sync::{Arc, Mutex, PoisonError};
603 use std::thread;
604 use std::time::{Duration, Instant};
605
606 use serde_json::Value;
607
608 use super::{
609 OperatorCredentialReissueRefusal, OperatorCredentialReissueRequest,
610 OperatorCredentialReissuer, ServedState, response_for_request, start_health_server,
611 };
612 use crate::health::checks::{
613 ClusterReadiness, ReadinessCondition, ReadinessState, SharedReadinessState,
614 };
615
616 fn loopback_ephemeral() -> Result<SocketAddr, Box<dyn std::error::Error>> {
617 Ok("127.0.0.1:0".parse()?)
618 }
619
620 fn served(readiness: SharedReadinessState) -> ServedState {
624 ServedState {
625 readiness,
626 unloadable: crate::health::unloadable::SharedUnloadableConversations::default(),
627 reissue: crate::health::reissue::SharedOperatorCredentialReissue::default(),
628 }
629 }
630
631 fn get(address: SocketAddr, path: &str) -> Result<String, Box<dyn std::error::Error>> {
632 let mut stream = TcpStream::connect(address)?;
633 stream.set_read_timeout(Some(Duration::from_secs(2)))?;
634 let request = format!("GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n");
635 stream.write_all(request.as_bytes())?;
636
637 let mut response = String::new();
638 stream.read_to_string(&mut response)?;
639 Ok(response)
640 }
641
642 fn assert_status(response: &str, status: u16) {
643 let expected = format!("HTTP/1.1 {status} ");
644 assert!(
645 response.starts_with(&expected),
646 "response status did not start with {expected}: {response}"
647 );
648 }
649
650 fn body(response: &str) -> Result<&str, Box<dyn std::error::Error>> {
651 let Some((_headers, body)) = response.split_once("\r\n\r\n") else {
652 return Err("response did not contain a header/body separator".into());
653 };
654 Ok(body)
655 }
656
657 fn json_body(response: &str) -> Result<Value, Box<dyn std::error::Error>> {
658 Ok(serde_json::from_str(body(response)?)?)
659 }
660
661 #[test]
662 fn health_endpoint_returns_json_200_regardless_of_readiness()
663 -> Result<(), Box<dyn std::error::Error>> {
664 let readiness = SharedReadinessState::new(ReadinessState::default());
665 let server = start_health_server(loopback_ephemeral()?, readiness)?;
666
667 let response = get(server.local_addr(), "/health")?;
668 server.shutdown()?;
669
670 assert_status(&response, 200);
671 assert!(response.contains("Content-Type: application/json\r\n"));
672 let body = json_body(&response)?;
673 assert_eq!(body["status"], "healthy");
674
675 Ok(())
676 }
677
678 #[test]
679 fn ready_endpoint_returns_503_before_main_listener_binds()
680 -> Result<(), Box<dyn std::error::Error>> {
681 let readiness = SharedReadinessState::new(ReadinessState::new(
682 true,
683 false,
684 ClusterReadiness::NotConfigured,
685 ));
686 let server = start_health_server(loopback_ephemeral()?, readiness)?;
687
688 let response = get(server.local_addr(), "/ready")?;
689 server.shutdown()?;
690
691 assert_status(&response, 503);
692 assert!(response.contains("Content-Type: application/json\r\n"));
693 let body = json_body(&response)?;
694 assert_eq!(body["ready"], false);
695 assert_eq!(body["unmet_conditions"][0], "listener_bound");
696
697 Ok(())
698 }
699
700 #[test]
701 fn ready_endpoint_returns_200_after_all_startup_gates() -> Result<(), Box<dyn std::error::Error>>
702 {
703 let readiness = SharedReadinessState::new(ReadinessState::ready_without_cluster());
704 let server = start_health_server(loopback_ephemeral()?, readiness)?;
705
706 let response = get(server.local_addr(), "/ready")?;
707 server.shutdown()?;
708
709 assert_status(&response, 200);
710 let body = json_body(&response)?;
711 assert_eq!(body["ready"], true);
712 let Some(unmet_conditions) = body["unmet_conditions"].as_array() else {
713 return Err("unmet_conditions should be an array".into());
714 };
715 assert!(unmet_conditions.is_empty());
716
717 Ok(())
718 }
719
720 #[test]
721 fn ready_endpoint_updates_from_shared_readiness_state() -> Result<(), Box<dyn std::error::Error>>
722 {
723 let readiness = SharedReadinessState::new(ReadinessState::default());
724 let server = start_health_server(loopback_ephemeral()?, readiness.clone())?;
725
726 let response = get(server.local_addr(), "/ready")?;
727 assert_status(&response, 503);
728
729 readiness.set_config_loaded(true);
730 readiness.set_listener_bound(true);
731 let response = get(server.local_addr(), "/ready")?;
732 server.shutdown()?;
733
734 assert_status(&response, 200);
735
736 Ok(())
737 }
738
739 #[test]
740 fn clustered_ready_transitions_503_to_200_when_membership_established()
741 -> Result<(), Box<dyn std::error::Error>> {
742 let readiness = SharedReadinessState::new(ReadinessState::new(
745 true,
746 true,
747 ClusterReadiness::Configured {
748 membership_established: false,
749 },
750 ));
751 let server = start_health_server(loopback_ephemeral()?, readiness.clone())?;
752
753 let response = get(server.local_addr(), "/ready")?;
754 assert_status(&response, 503);
755 let body = json_body(&response)?;
756 assert_eq!(body["ready"], false);
757 assert_eq!(
758 body["unmet_conditions"][0],
759 serde_json::to_value(ReadinessCondition::ClusterMembershipEstablished)?
760 );
761
762 readiness.set_cluster_membership_established(true);
765 let response = get(server.local_addr(), "/ready")?;
766 server.shutdown()?;
767
768 assert_status(&response, 200);
769 let body = json_body(&response)?;
770 assert_eq!(body["ready"], true);
771 let Some(unmet_conditions) = body["unmet_conditions"].as_array() else {
772 return Err("unmet_conditions should be an array".into());
773 };
774 assert!(unmet_conditions.is_empty());
775
776 Ok(())
777 }
778
779 #[test]
780 fn cluster_readiness_is_listed_when_configured_but_not_joined()
781 -> Result<(), Box<dyn std::error::Error>> {
782 let readiness = SharedReadinessState::new(ReadinessState::new(
783 true,
784 true,
785 ClusterReadiness::Configured {
786 membership_established: false,
787 },
788 ));
789 let response = response_for_request(b"GET /ready HTTP/1.1\r\n\r\n", &served(readiness))?;
790 let response = String::from_utf8(response)?;
791
792 assert_status(&response, 503);
793 let body = json_body(&response)?;
794 assert_eq!(
795 body["unmet_conditions"][0],
796 serde_json::to_value(ReadinessCondition::ClusterMembershipEstablished)?
797 );
798
799 Ok(())
800 }
801
802 #[test]
820 fn unloadable_conversations_route_answers_the_operator_a_json_shape()
821 -> Result<(), Box<dyn std::error::Error>> {
822 let readiness = SharedReadinessState::new(ReadinessState::default());
823 let server = start_health_server(loopback_ephemeral()?, readiness)?;
824
825 let response = get(server.local_addr(), "/unloadable-conversations")?;
826 let unknown = get(server.local_addr(), "/unloadable-conversations-typo")?;
827 server.shutdown()?;
828
829 assert_status(&unknown, 404);
832
833 assert_status(&response, 200);
834 assert!(
835 response.contains("Content-Type: application/json\r\n"),
836 "the unloadable-conversations route must answer JSON: {response}"
837 );
838 let body = json_body(&response)?;
839 assert_eq!(
840 body["count"], 0,
841 "a server with no participant record attached refuses nothing: {body}"
842 );
843 assert_eq!(
844 body["participant_installed"], false,
845 "no participant record is attached to this server, and the surface must say so \
846 rather than let a zero count read as a clean node: {body}"
847 );
848 let Some(conversations) = body["conversations"].as_array() else {
849 return Err("conversations should be an array".into());
850 };
851 assert!(
852 conversations.is_empty(),
853 "no conversation was refused: {conversations:?}"
854 );
855
856 Ok(())
857 }
858
859 #[test]
860 fn unsupported_paths_are_not_served() -> Result<(), Box<dyn std::error::Error>> {
861 let readiness = SharedReadinessState::default();
862 let response = response_for_request(b"GET /unknown HTTP/1.1\r\n\r\n", &served(readiness))?;
863 let response = String::from_utf8(response)?;
864
865 assert_status(&response, 404);
866
867 Ok(())
868 }
869
870 #[test]
871 fn unsupported_methods_on_health_paths_are_rejected() -> Result<(), Box<dyn std::error::Error>>
872 {
873 let readiness = SharedReadinessState::default();
874 let response = response_for_request(b"POST /health HTTP/1.1\r\n\r\n", &served(readiness))?;
875 let response = String::from_utf8(response)?;
876
877 assert_status(&response, 405);
878
879 Ok(())
880 }
881
882 #[derive(Debug)]
890 struct RecordingReissuer {
891 outcome: crate::health::reissue::OperatorCredentialReissueOutcome,
892 seen: Mutex<Vec<OperatorCredentialReissueRequest>>,
893 }
894
895 impl OperatorCredentialReissuer for RecordingReissuer {
896 fn reissue(
897 &self,
898 request: OperatorCredentialReissueRequest,
899 ) -> Result<
900 crate::health::reissue::OperatorCredentialReissueOutcome,
901 crate::health::reissue::OperatorCredentialReissueError,
902 > {
903 self.seen
904 .lock()
905 .unwrap_or_else(PoisonError::into_inner)
906 .push(request);
907 Ok(self.outcome.clone())
908 }
909 }
910
911 fn served_with_reissuer(
912 outcome: crate::health::reissue::OperatorCredentialReissueOutcome,
913 ) -> (ServedState, Arc<RecordingReissuer>) {
914 let reissuer = Arc::new(RecordingReissuer {
915 outcome,
916 seen: Mutex::new(Vec::new()),
917 });
918 let state = served(SharedReadinessState::default());
919 state
920 .reissue
921 .install(Arc::clone(&reissuer) as Arc<dyn OperatorCredentialReissuer>);
922 (state, reissuer)
923 }
924
925 const REISSUE_TARGET: &str = "/operator/credential-reissue?conversation_id=7&participant_id=3&\
926 expected_current_generation=14";
927
928 #[test]
931 fn the_reissue_route_reports_an_uninstalled_participant()
932 -> Result<(), Box<dyn std::error::Error>> {
933 let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
934 let response =
935 response_for_request(request.as_bytes(), &served(SharedReadinessState::default()))?;
936 let response = String::from_utf8(response)?;
937
938 assert_status(&response, 503);
939 Ok(())
940 }
941
942 #[test]
945 fn the_reissue_route_carries_the_three_inputs_and_returns_the_secret_once()
946 -> Result<(), Box<dyn std::error::Error>> {
947 let issued = crate::health::reissue::OperatorCredentialReissued {
948 conversation_id: 7,
949 participant_id: 3,
950 presented_generation: 14,
951 issued_generation: 15,
952 attach_secret: crate::health::reissue::encode_hex(&[0x5A; 32]),
953 };
954 let (state, reissuer) = served_with_reissuer(
955 crate::health::reissue::OperatorCredentialReissueOutcome::Issued(issued),
956 );
957 let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
958
959 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
960
961 assert_status(&response, 200);
962 let body = json_body(&response)?;
963 assert_eq!(body["issued_generation"], 15);
964 assert_eq!(body["presented_generation"], 14);
965 assert_eq!(body["attach_secret"], "5a".repeat(32));
966 let seen = reissuer
967 .seen
968 .lock()
969 .unwrap_or_else(PoisonError::into_inner)
970 .clone();
971 assert_eq!(
972 seen.as_slice(),
973 [OperatorCredentialReissueRequest {
974 conversation_id: 7,
975 participant_id: 3,
976 expected_current_generation: 14,
977 }]
978 );
979 Ok(())
980 }
981
982 #[test]
985 fn the_reissue_route_serves_the_normative_generation_pair()
986 -> Result<(), Box<dyn std::error::Error>> {
987 let (state, _) = served_with_reissuer(
988 crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
989 OperatorCredentialReissueRefusal::GenerationMismatch {
990 conversation_id: 7,
991 participant_id: 3,
992 presented_generation: 14,
993 current_generation: 15,
994 },
995 ),
996 );
997 let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
998
999 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1000
1001 assert_status(&response, 409);
1002 let body = json_body(&response)?;
1003 assert_eq!(body["refusal"], "generation_mismatch");
1004 assert_eq!(body["presented_generation"], 14);
1005 assert_eq!(body["current_generation"], 15);
1006 Ok(())
1007 }
1008
1009 #[test]
1012 fn the_reissue_route_answers_a_lookup_miss_with_not_found()
1013 -> Result<(), Box<dyn std::error::Error>> {
1014 let (state, _) = served_with_reissuer(
1015 crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
1016 OperatorCredentialReissueRefusal::ConversationUnknown { conversation_id: 7 },
1017 ),
1018 );
1019 let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
1020
1021 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1022
1023 assert_status(&response, 404);
1024 let body = json_body(&response)?;
1025 assert_eq!(body["refusal"], "conversation_unknown");
1026 assert_eq!(body["conversation_id"], 7);
1027 assert!(
1028 body.get("participant_id").is_none(),
1029 "an unknown-conversation refusal must disclose nothing beyond the presented \
1030 conversation id: {body}"
1031 );
1032 Ok(())
1033 }
1034
1035 #[test]
1039 fn a_malformed_reissue_call_is_refused_and_never_reaches_the_authority()
1040 -> Result<(), Box<dyn std::error::Error>> {
1041 let (state, reissuer) = served_with_reissuer(
1042 crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
1043 OperatorCredentialReissueRefusal::ConversationUnknown { conversation_id: 7 },
1044 ),
1045 );
1046 let malformed = [
1047 "/operator/credential-reissue?conversation_id=7&participant_id=3",
1049 "/operator/credential-reissue",
1051 "/operator/credential-reissue?conversation_id=7&participant_id=3&\
1053 expected_current_generation=fourteen",
1054 "/operator/credential-reissue?conversation_id=7&participant_id=3&\
1056 expected_current_generation=14&force=1",
1057 "/operator/credential-reissue?conversation_id=7&conversation_id=8&\
1060 participant_id=3&expected_current_generation=14",
1061 ];
1062
1063 for target in malformed {
1064 let request = format!("POST {target} HTTP/1.1\r\n\r\n");
1065 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1066 assert_status(&response, 400);
1067 }
1068
1069 assert!(
1070 reissuer
1071 .seen
1072 .lock()
1073 .unwrap_or_else(PoisonError::into_inner)
1074 .is_empty(),
1075 "a malformed call must never reach the serialized participant-state point"
1076 );
1077 Ok(())
1078 }
1079
1080 #[test]
1083 fn the_reissue_route_refuses_every_other_method() -> Result<(), Box<dyn std::error::Error>> {
1084 let (state, reissuer) = served_with_reissuer(
1085 crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
1086 OperatorCredentialReissueRefusal::ConversationUnknown { conversation_id: 7 },
1087 ),
1088 );
1089
1090 for method in ["GET", "PUT", "DELETE"] {
1091 let request = format!("{method} {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
1092 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1093 assert_status(&response, 405);
1094 }
1095
1096 assert!(
1097 reissuer
1098 .seen
1099 .lock()
1100 .unwrap_or_else(PoisonError::into_inner)
1101 .is_empty()
1102 );
1103 Ok(())
1104 }
1105
1106 #[test]
1110 fn the_reissue_route_did_not_move_any_existing_routes_answer()
1111 -> Result<(), Box<dyn std::error::Error>> {
1112 let state = served(SharedReadinessState::default());
1113 for target in [
1114 "/health?x=1",
1115 "/ready?x=1",
1116 "/metrics?x=1",
1117 "/unloadable-conversations?x=1",
1118 ] {
1119 let request = format!("GET {target} HTTP/1.1\r\n\r\n");
1120 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1121 assert_status(&response, 404);
1122 }
1123 let response = String::from_utf8(response_for_request(
1127 b"GET /health HTTP/1.1\r\n\r\n",
1128 &state,
1129 )?)?;
1130 assert_status(&response, 200);
1131 Ok(())
1132 }
1133
1134 #[test]
1139 fn silent_health_listener_has_zero_application_wakes() -> Result<(), Box<dyn std::error::Error>>
1140 {
1141 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1142
1143 let deadline = Instant::now() + Duration::from_secs(2);
1144 while server.accept_attempts() < 1 && Instant::now() < deadline {
1145 thread::sleep(Duration::from_millis(5));
1146 }
1147 let armed = server.accept_attempts();
1148 assert_eq!(
1149 armed, 1,
1150 "the blocking accept is issued exactly once when parked"
1151 );
1152
1153 thread::sleep(Duration::from_millis(200));
1154 assert_eq!(
1155 server.accept_attempts(),
1156 armed,
1157 "a silent health listener must not wake or re-accept"
1158 );
1159 assert_eq!(
1160 server.shed_count(),
1161 0,
1162 "a silent health listener sheds nothing"
1163 );
1164
1165 let response = get(server.local_addr(), "/health")?;
1167 assert_status(&response, 200);
1168 let body = json_body(&response)?;
1169 assert_eq!(body["status"], "healthy");
1170
1171 server.shutdown()?;
1172 Ok(())
1173 }
1174
1175 #[test]
1179 fn health_accept_source_has_no_wouldblock_sleep_poll() {
1180 const SOURCE: &str = include_str!("endpoint.rs");
1181 let production = SOURCE.split("mod tests").next().unwrap_or(SOURCE);
1182 for forbidden in [
1183 "set_nonblocking(true)",
1184 "ErrorKind::WouldBlock",
1185 "thread::sleep",
1186 ] {
1187 assert!(
1188 !production.contains(forbidden),
1189 "retired health accept-path source `{forbidden}` reappeared"
1190 );
1191 }
1192 }
1193
1194 #[test]
1200 fn health_shutdown_interrupts_accept_wait() -> Result<(), Box<dyn std::error::Error>> {
1201 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1203 let start = Instant::now();
1204 server.shutdown()?;
1205 assert!(
1206 start.elapsed() < Duration::from_secs(2),
1207 "shutdown before arming must interrupt promptly, not sleep-poll"
1208 );
1209
1210 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1212 let deadline = Instant::now() + Duration::from_secs(2);
1213 while server.accept_attempts() < 1 && Instant::now() < deadline {
1214 thread::sleep(Duration::from_millis(5));
1215 }
1216 assert_eq!(
1217 server.accept_attempts(),
1218 1,
1219 "the worker parked before shutdown"
1220 );
1221 let parked_addr = server.local_addr();
1222 let start = Instant::now();
1223 server.shutdown()?;
1224 assert!(
1225 start.elapsed() < Duration::from_secs(2),
1226 "shutdown of a parked accept must interrupt promptly"
1227 );
1228 assert!(
1230 TcpStream::connect(parked_addr).is_err(),
1231 "the listener descriptor was released; further connects are refused"
1232 );
1233
1234 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1244 let deadline = Instant::now() + Duration::from_secs(2);
1245 while server.accept_attempts() < 1 && Instant::now() < deadline {
1246 thread::sleep(Duration::from_millis(5));
1247 }
1248 let _pending = TcpStream::connect(server.local_addr())?;
1249 let start = Instant::now();
1250 server.shutdown()?;
1251 assert!(
1252 start.elapsed() < Duration::from_millis(500),
1253 "shutdown concurrent with a pending accept must interrupt promptly (TOLD), \
1254 not defer by a request read deadline: elapsed {:?}",
1255 start.elapsed()
1256 );
1257
1258 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1260 let response = get(server.local_addr(), "/health")?;
1261 assert_status(&response, 200);
1262 let start = Instant::now();
1263 server.shutdown()?;
1264 assert!(
1265 start.elapsed() < Duration::from_secs(2),
1266 "shutdown after a served request must interrupt the next parked accept promptly"
1267 );
1268
1269 Ok(())
1270 }
1271
1272 #[test]
1288 fn shutdown_interrupts_in_flight_silent_request_read() -> Result<(), Box<dyn std::error::Error>>
1289 {
1290 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1291
1292 let _silent = TcpStream::connect(server.local_addr())?;
1297
1298 let deadline = Instant::now() + Duration::from_secs(2);
1301 while server.requests_entered() < 1 && Instant::now() < deadline {
1302 thread::sleep(Duration::from_millis(5));
1303 }
1304 assert_eq!(
1305 server.requests_entered(),
1306 1,
1307 "the worker entered the in-flight silent request read"
1308 );
1309
1310 let start = Instant::now();
1311 server.shutdown()?;
1312 assert!(
1313 start.elapsed() < Duration::from_millis(500),
1314 "shutdown must interrupt an in-flight silent request read promptly (TOLD), \
1315 not defer by the read's admitted 2s deadline: elapsed {:?}",
1316 start.elapsed()
1317 );
1318
1319 Ok(())
1320 }
1321
1322 #[test]
1329 fn health_idle_grows_unrelated_counters_while_accept_stays_flat()
1330 -> Result<(), Box<dyn std::error::Error>> {
1331 let idle = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1332 let busy = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1333
1334 let deadline = Instant::now() + Duration::from_secs(2);
1335 while (idle.accept_attempts() < 1 || busy.accept_attempts() < 1)
1336 && Instant::now() < deadline
1337 {
1338 thread::sleep(Duration::from_millis(5));
1339 }
1340 let idle_armed = idle.accept_attempts();
1341 assert_eq!(idle_armed, 1, "the idle listener parks exactly one accept");
1342 let busy_before = busy.accept_attempts();
1343
1344 for _ in 0..5 {
1347 let response = get(busy.local_addr(), "/health")?;
1348 assert_status(&response, 200);
1349 }
1350 let deadline = Instant::now() + Duration::from_secs(2);
1351 while busy.accept_attempts() <= busy_before && Instant::now() < deadline {
1352 thread::sleep(Duration::from_millis(5));
1353 }
1354
1355 assert!(
1356 busy.accept_attempts() > busy_before,
1357 "an unrelated served request grows the busy listener's accept counter"
1358 );
1359 assert_eq!(
1360 idle.accept_attempts(),
1361 idle_armed,
1362 "the silent listener's accept counter stays flat during the workload"
1363 );
1364 assert_eq!(idle.shed_count(), 0, "the silent listener sheds nothing");
1365
1366 idle.shutdown()?;
1367 busy.shutdown()?;
1368 Ok(())
1369 }
1370}