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 =
926 "/operator/credential-reissue?conversation_id=7&participant_id=3&\
927 expected_current_generation=14";
928
929 #[test]
932 fn the_reissue_route_reports_an_uninstalled_participant()
933 -> Result<(), Box<dyn std::error::Error>> {
934 let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
935 let response =
936 response_for_request(request.as_bytes(), &served(SharedReadinessState::default()))?;
937 let response = String::from_utf8(response)?;
938
939 assert_status(&response, 503);
940 Ok(())
941 }
942
943 #[test]
946 fn the_reissue_route_carries_the_three_inputs_and_returns_the_secret_once()
947 -> Result<(), Box<dyn std::error::Error>> {
948 let issued = crate::health::reissue::OperatorCredentialReissued {
949 conversation_id: 7,
950 participant_id: 3,
951 presented_generation: 14,
952 issued_generation: 15,
953 attach_secret: crate::health::reissue::encode_hex(&[0x5A; 32]),
954 };
955 let (state, reissuer) = served_with_reissuer(
956 crate::health::reissue::OperatorCredentialReissueOutcome::Issued(issued),
957 );
958 let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
959
960 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
961
962 assert_status(&response, 200);
963 let body = json_body(&response)?;
964 assert_eq!(body["issued_generation"], 15);
965 assert_eq!(body["presented_generation"], 14);
966 assert_eq!(body["attach_secret"], "5a".repeat(32));
967 let seen = reissuer
968 .seen
969 .lock()
970 .unwrap_or_else(PoisonError::into_inner)
971 .clone();
972 assert_eq!(
973 seen.as_slice(),
974 [OperatorCredentialReissueRequest {
975 conversation_id: 7,
976 participant_id: 3,
977 expected_current_generation: 14,
978 }]
979 );
980 Ok(())
981 }
982
983 #[test]
986 fn the_reissue_route_serves_the_normative_generation_pair()
987 -> Result<(), Box<dyn std::error::Error>> {
988 let (state, _) = served_with_reissuer(
989 crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
990 OperatorCredentialReissueRefusal::GenerationMismatch {
991 conversation_id: 7,
992 participant_id: 3,
993 presented_generation: 14,
994 current_generation: 15,
995 },
996 ),
997 );
998 let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
999
1000 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1001
1002 assert_status(&response, 409);
1003 let body = json_body(&response)?;
1004 assert_eq!(body["refusal"], "generation_mismatch");
1005 assert_eq!(body["presented_generation"], 14);
1006 assert_eq!(body["current_generation"], 15);
1007 Ok(())
1008 }
1009
1010 #[test]
1013 fn the_reissue_route_answers_a_lookup_miss_with_not_found()
1014 -> Result<(), Box<dyn std::error::Error>> {
1015 let (state, _) = served_with_reissuer(
1016 crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
1017 OperatorCredentialReissueRefusal::ConversationUnknown { conversation_id: 7 },
1018 ),
1019 );
1020 let request = format!("POST {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
1021
1022 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1023
1024 assert_status(&response, 404);
1025 let body = json_body(&response)?;
1026 assert_eq!(body["refusal"], "conversation_unknown");
1027 assert_eq!(body["conversation_id"], 7);
1028 assert!(
1029 body.get("participant_id").is_none(),
1030 "an unknown-conversation refusal must disclose nothing beyond the presented \
1031 conversation id: {body}"
1032 );
1033 Ok(())
1034 }
1035
1036 #[test]
1040 fn a_malformed_reissue_call_is_refused_and_never_reaches_the_authority()
1041 -> Result<(), Box<dyn std::error::Error>> {
1042 let (state, reissuer) = served_with_reissuer(
1043 crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
1044 OperatorCredentialReissueRefusal::ConversationUnknown { conversation_id: 7 },
1045 ),
1046 );
1047 let malformed = [
1048 "/operator/credential-reissue?conversation_id=7&participant_id=3",
1050 "/operator/credential-reissue",
1052 "/operator/credential-reissue?conversation_id=7&participant_id=3&\
1054 expected_current_generation=fourteen",
1055 "/operator/credential-reissue?conversation_id=7&participant_id=3&\
1057 expected_current_generation=14&force=1",
1058 "/operator/credential-reissue?conversation_id=7&conversation_id=8&\
1061 participant_id=3&expected_current_generation=14",
1062 ];
1063
1064 for target in malformed {
1065 let request = format!("POST {target} HTTP/1.1\r\n\r\n");
1066 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1067 assert_status(&response, 400);
1068 }
1069
1070 assert!(
1071 reissuer
1072 .seen
1073 .lock()
1074 .unwrap_or_else(PoisonError::into_inner)
1075 .is_empty(),
1076 "a malformed call must never reach the serialized participant-state point"
1077 );
1078 Ok(())
1079 }
1080
1081 #[test]
1084 fn the_reissue_route_refuses_every_other_method() -> Result<(), Box<dyn std::error::Error>> {
1085 let (state, reissuer) = served_with_reissuer(
1086 crate::health::reissue::OperatorCredentialReissueOutcome::Refused(
1087 OperatorCredentialReissueRefusal::ConversationUnknown { conversation_id: 7 },
1088 ),
1089 );
1090
1091 for method in ["GET", "PUT", "DELETE"] {
1092 let request = format!("{method} {REISSUE_TARGET} HTTP/1.1\r\n\r\n");
1093 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1094 assert_status(&response, 405);
1095 }
1096
1097 assert!(
1098 reissuer
1099 .seen
1100 .lock()
1101 .unwrap_or_else(PoisonError::into_inner)
1102 .is_empty()
1103 );
1104 Ok(())
1105 }
1106
1107 #[test]
1111 fn the_reissue_route_did_not_move_any_existing_routes_answer()
1112 -> Result<(), Box<dyn std::error::Error>> {
1113 let state = served(SharedReadinessState::default());
1114 for target in [
1115 "/health?x=1",
1116 "/ready?x=1",
1117 "/metrics?x=1",
1118 "/unloadable-conversations?x=1",
1119 ] {
1120 let request = format!("GET {target} HTTP/1.1\r\n\r\n");
1121 let response = String::from_utf8(response_for_request(request.as_bytes(), &state)?)?;
1122 assert_status(&response, 404);
1123 }
1124 let response =
1128 String::from_utf8(response_for_request(b"GET /health HTTP/1.1\r\n\r\n", &state)?)?;
1129 assert_status(&response, 200);
1130 Ok(())
1131 }
1132
1133 #[test]
1138 fn silent_health_listener_has_zero_application_wakes() -> Result<(), Box<dyn std::error::Error>>
1139 {
1140 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1141
1142 let deadline = Instant::now() + Duration::from_secs(2);
1143 while server.accept_attempts() < 1 && Instant::now() < deadline {
1144 thread::sleep(Duration::from_millis(5));
1145 }
1146 let armed = server.accept_attempts();
1147 assert_eq!(
1148 armed, 1,
1149 "the blocking accept is issued exactly once when parked"
1150 );
1151
1152 thread::sleep(Duration::from_millis(200));
1153 assert_eq!(
1154 server.accept_attempts(),
1155 armed,
1156 "a silent health listener must not wake or re-accept"
1157 );
1158 assert_eq!(
1159 server.shed_count(),
1160 0,
1161 "a silent health listener sheds nothing"
1162 );
1163
1164 let response = get(server.local_addr(), "/health")?;
1166 assert_status(&response, 200);
1167 let body = json_body(&response)?;
1168 assert_eq!(body["status"], "healthy");
1169
1170 server.shutdown()?;
1171 Ok(())
1172 }
1173
1174 #[test]
1178 fn health_accept_source_has_no_wouldblock_sleep_poll() {
1179 const SOURCE: &str = include_str!("endpoint.rs");
1180 let production = SOURCE.split("mod tests").next().unwrap_or(SOURCE);
1181 for forbidden in [
1182 "set_nonblocking(true)",
1183 "ErrorKind::WouldBlock",
1184 "thread::sleep",
1185 ] {
1186 assert!(
1187 !production.contains(forbidden),
1188 "retired health accept-path source `{forbidden}` reappeared"
1189 );
1190 }
1191 }
1192
1193 #[test]
1199 fn health_shutdown_interrupts_accept_wait() -> Result<(), Box<dyn std::error::Error>> {
1200 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1202 let start = Instant::now();
1203 server.shutdown()?;
1204 assert!(
1205 start.elapsed() < Duration::from_secs(2),
1206 "shutdown before arming must interrupt promptly, not sleep-poll"
1207 );
1208
1209 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1211 let deadline = Instant::now() + Duration::from_secs(2);
1212 while server.accept_attempts() < 1 && Instant::now() < deadline {
1213 thread::sleep(Duration::from_millis(5));
1214 }
1215 assert_eq!(
1216 server.accept_attempts(),
1217 1,
1218 "the worker parked before shutdown"
1219 );
1220 let parked_addr = server.local_addr();
1221 let start = Instant::now();
1222 server.shutdown()?;
1223 assert!(
1224 start.elapsed() < Duration::from_secs(2),
1225 "shutdown of a parked accept must interrupt promptly"
1226 );
1227 assert!(
1229 TcpStream::connect(parked_addr).is_err(),
1230 "the listener descriptor was released; further connects are refused"
1231 );
1232
1233 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1243 let deadline = Instant::now() + Duration::from_secs(2);
1244 while server.accept_attempts() < 1 && Instant::now() < deadline {
1245 thread::sleep(Duration::from_millis(5));
1246 }
1247 let _pending = TcpStream::connect(server.local_addr())?;
1248 let start = Instant::now();
1249 server.shutdown()?;
1250 assert!(
1251 start.elapsed() < Duration::from_millis(500),
1252 "shutdown concurrent with a pending accept must interrupt promptly (TOLD), \
1253 not defer by a request read deadline: elapsed {:?}",
1254 start.elapsed()
1255 );
1256
1257 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1259 let response = get(server.local_addr(), "/health")?;
1260 assert_status(&response, 200);
1261 let start = Instant::now();
1262 server.shutdown()?;
1263 assert!(
1264 start.elapsed() < Duration::from_secs(2),
1265 "shutdown after a served request must interrupt the next parked accept promptly"
1266 );
1267
1268 Ok(())
1269 }
1270
1271 #[test]
1287 fn shutdown_interrupts_in_flight_silent_request_read() -> Result<(), Box<dyn std::error::Error>>
1288 {
1289 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1290
1291 let _silent = TcpStream::connect(server.local_addr())?;
1296
1297 let deadline = Instant::now() + Duration::from_secs(2);
1300 while server.requests_entered() < 1 && Instant::now() < deadline {
1301 thread::sleep(Duration::from_millis(5));
1302 }
1303 assert_eq!(
1304 server.requests_entered(),
1305 1,
1306 "the worker entered the in-flight silent request read"
1307 );
1308
1309 let start = Instant::now();
1310 server.shutdown()?;
1311 assert!(
1312 start.elapsed() < Duration::from_millis(500),
1313 "shutdown must interrupt an in-flight silent request read promptly (TOLD), \
1314 not defer by the read's admitted 2s deadline: elapsed {:?}",
1315 start.elapsed()
1316 );
1317
1318 Ok(())
1319 }
1320
1321 #[test]
1328 fn health_idle_grows_unrelated_counters_while_accept_stays_flat()
1329 -> Result<(), Box<dyn std::error::Error>> {
1330 let idle = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1331 let busy = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
1332
1333 let deadline = Instant::now() + Duration::from_secs(2);
1334 while (idle.accept_attempts() < 1 || busy.accept_attempts() < 1)
1335 && Instant::now() < deadline
1336 {
1337 thread::sleep(Duration::from_millis(5));
1338 }
1339 let idle_armed = idle.accept_attempts();
1340 assert_eq!(idle_armed, 1, "the idle listener parks exactly one accept");
1341 let busy_before = busy.accept_attempts();
1342
1343 for _ in 0..5 {
1346 let response = get(busy.local_addr(), "/health")?;
1347 assert_status(&response, 200);
1348 }
1349 let deadline = Instant::now() + Duration::from_secs(2);
1350 while busy.accept_attempts() <= busy_before && Instant::now() < deadline {
1351 thread::sleep(Duration::from_millis(5));
1352 }
1353
1354 assert!(
1355 busy.accept_attempts() > busy_before,
1356 "an unrelated served request grows the busy listener's accept counter"
1357 );
1358 assert_eq!(
1359 idle.accept_attempts(),
1360 idle_armed,
1361 "the silent listener's accept counter stays flat during the workload"
1362 );
1363 assert_eq!(idle.shed_count(), 0, "the silent listener sheds nothing");
1364
1365 idle.shutdown()?;
1366 busy.shutdown()?;
1367 Ok(())
1368 }
1369}