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::{loopback_interrupt_target, shed_on_fd_exhaustion};
10
11use super::checks::{SharedReadinessState, health_check, readiness_check};
12use super::unloadable::{SharedUnloadableConversations, UnloadableConversationRecord};
13
14use super::metrics_route;
15
16const HEALTH_PATH: &str = "/health";
17const READY_PATH: &str = "/ready";
18const METRICS_PATH: &str = "/metrics";
19const UNLOADABLE_PATH: &str = "/unloadable-conversations";
20const APPLICATION_JSON: &str = "application/json";
21const READ_BUFFER_BYTES: usize = 2048;
22
23#[derive(Debug)]
33pub struct HealthServerHandle {
34 local_addr: SocketAddr,
35 interrupt_target: SocketAddr,
38 shutdown: Arc<AtomicBool>,
39 active_stream: Arc<Mutex<Option<TcpStream>>>,
47 unloadable: SharedUnloadableConversations,
53 worker: Option<JoinHandle<Result<(), ServerError>>>,
54 #[cfg(test)]
59 accept_attempts: Arc<AtomicU64>,
60 #[cfg(test)]
63 shed_count: Arc<AtomicU64>,
64 #[cfg(test)]
69 requests_entered: Arc<AtomicU64>,
70}
71
72impl HealthServerHandle {
73 #[must_use]
75 pub const fn local_addr(&self) -> SocketAddr {
76 self.local_addr
77 }
78
79 pub fn install_unloadable_record(&self, record: UnloadableConversationRecord) {
92 self.unloadable.install(record);
93 }
94
95 pub fn shutdown(mut self) -> Result<(), ServerError> {
102 self.stop_worker()
103 }
104
105 fn stop_worker(&mut self) -> Result<(), ServerError> {
106 self.shutdown.store(true, Ordering::SeqCst);
107 let Some(worker) = self.worker.take() else {
108 return Ok(());
109 };
110 let in_flight = self
119 .active_stream
120 .lock()
121 .unwrap_or_else(PoisonError::into_inner)
122 .take();
123 if let Some(stream) = in_flight {
124 let _ = stream.shutdown(Shutdown::Both);
125 }
126 if let Ok(waker) = TcpStream::connect(self.interrupt_target) {
134 drop(waker);
135 }
136
137 worker.join().map_err(|_| ServerError::HealthEndpoint {
138 message: "health endpoint worker thread terminated unexpectedly".to_owned(),
139 })?
140 }
141
142 #[cfg(test)]
144 fn accept_attempts(&self) -> u64 {
145 self.accept_attempts.load(Ordering::SeqCst)
146 }
147
148 #[cfg(test)]
150 fn shed_count(&self) -> u64 {
151 self.shed_count.load(Ordering::SeqCst)
152 }
153
154 #[cfg(test)]
156 fn requests_entered(&self) -> u64 {
157 self.requests_entered.load(Ordering::SeqCst)
158 }
159}
160
161impl Drop for HealthServerHandle {
162 fn drop(&mut self) {
163 if let Err(error) = self.stop_worker() {
164 tracing::debug!(%error, "health endpoint shutdown during drop failed");
165 }
166 }
167}
168
169pub fn start_health_server(
181 bind_address: SocketAddr,
182 readiness: SharedReadinessState,
183) -> Result<HealthServerHandle, ServerError> {
184 let listener =
185 TcpListener::bind(bind_address).map_err(|error| ServerError::HealthEndpoint {
186 message: format!("failed to bind health endpoint at {bind_address}: {error}"),
187 })?;
188 let local_addr = listener
192 .local_addr()
193 .map_err(|error| ServerError::HealthEndpoint {
194 message: format!("failed to inspect health endpoint listener address: {error}"),
195 })?;
196 let interrupt_target = loopback_interrupt_target(local_addr);
197 let shutdown = Arc::new(AtomicBool::new(false));
198 let active_stream = Arc::new(Mutex::new(None));
199 let unloadable = SharedUnloadableConversations::default();
200 let accept_attempts = Arc::new(AtomicU64::new(0));
201 let shed_count = Arc::new(AtomicU64::new(0));
202 let requests_entered = Arc::new(AtomicU64::new(0));
203 let worker_shutdown = Arc::clone(&shutdown);
204 let worker_stream = Arc::clone(&active_stream);
205 let worker_attempts = Arc::clone(&accept_attempts);
206 let worker_shed = Arc::clone(&shed_count);
207 let worker_entered = Arc::clone(&requests_entered);
208 let served = ServedState {
209 readiness,
210 unloadable: unloadable.clone(),
211 };
212 let worker = thread::spawn(move || {
213 serve(
214 &listener,
215 &served,
216 &worker_shutdown,
217 &worker_stream,
218 &worker_attempts,
219 &worker_shed,
220 &worker_entered,
221 )
222 });
223
224 Ok(HealthServerHandle {
225 local_addr,
226 interrupt_target,
227 shutdown,
228 active_stream,
229 unloadable,
230 worker: Some(worker),
231 #[cfg(test)]
232 accept_attempts,
233 #[cfg(test)]
234 shed_count,
235 #[cfg(test)]
236 requests_entered,
237 })
238}
239
240#[derive(Debug, Clone)]
243struct ServedState {
244 readiness: SharedReadinessState,
245 unloadable: SharedUnloadableConversations,
246}
247
248fn serve(
249 listener: &TcpListener,
250 served: &ServedState,
251 shutdown: &AtomicBool,
252 active_stream: &Mutex<Option<TcpStream>>,
253 accept_attempts: &AtomicU64,
254 shed_count: &AtomicU64,
255 requests_entered: &AtomicU64,
256) -> Result<(), ServerError> {
257 let mut reserve = listener.try_clone().ok();
260 while !shutdown.load(Ordering::SeqCst) {
261 accept_attempts.fetch_add(1, Ordering::SeqCst);
262 match listener.accept() {
263 Ok((stream, ..)) => {
264 let admitted = {
273 let mut slot = active_stream.lock().unwrap_or_else(PoisonError::into_inner);
274 if shutdown.load(Ordering::SeqCst) {
275 false
276 } else {
277 *slot = stream.try_clone().ok();
278 true
279 }
280 };
281 if !admitted {
282 drop(stream);
283 continue;
284 }
285 requests_entered.fetch_add(1, Ordering::SeqCst);
286 let result = handle_connection(stream, served);
292 *active_stream.lock().unwrap_or_else(PoisonError::into_inner) = None;
296 if let Err(error) = result {
297 tracing::debug!(%error, "health endpoint connection error");
298 }
299 }
300 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
301 Err(error) if is_transient_accept_error(&error) => {
302 shed_on_fd_exhaustion(listener, &mut reserve, shed_count, &error);
303 }
304 Err(error) => {
305 return Err(ServerError::HealthEndpoint {
306 message: format!("health endpoint accept failed: {error}"),
307 });
308 }
309 }
310 }
311
312 Ok(())
313}
314
315fn is_transient_accept_error(error: &std::io::Error) -> bool {
318 matches!(error.raw_os_error(), Some(code) if code == 24 || code == 23)
319}
320
321fn handle_connection(mut stream: TcpStream, served: &ServedState) -> Result<(), ServerError> {
322 stream
323 .set_nonblocking(false)
324 .map_err(|error| ServerError::HealthEndpoint {
325 message: format!("failed to configure health request stream: {error}"),
326 })?;
327 stream
328 .set_read_timeout(Some(Duration::from_secs(2)))
329 .map_err(|error| ServerError::HealthEndpoint {
330 message: format!("failed to set health request read timeout: {error}"),
331 })?;
332
333 let mut buffer = [0_u8; READ_BUFFER_BYTES];
334 let bytes_read = stream
335 .read(&mut buffer)
336 .map_err(|error| ServerError::HealthEndpoint {
337 message: format!("failed to read health request: {error}"),
338 })?;
339
340 if bytes_read == 0 {
341 return Ok(());
342 }
343
344 let response = response_for_request(&buffer[..bytes_read], served)?;
345 stream
346 .write_all(&response)
347 .map_err(|error| ServerError::HealthEndpoint {
348 message: format!("failed to write health response: {error}"),
349 })?;
350 stream.flush().map_err(|error| ServerError::HealthEndpoint {
351 message: format!("failed to flush health response: {error}"),
352 })
353}
354
355fn response_for_request(request: &[u8], served: &ServedState) -> Result<Vec<u8>, ServerError> {
356 let Ok(request) = std::str::from_utf8(request) else {
357 return Ok(empty_response(StatusCode::BadRequest));
358 };
359 let Some((method, path)) = parse_request_line(request) else {
360 return Ok(empty_response(StatusCode::BadRequest));
361 };
362
363 match (method, path) {
364 ("GET", HEALTH_PATH) => json_response(StatusCode::Ok, &health_check()),
365 ("GET", READY_PATH) => {
366 let status = readiness_check(&served.readiness.snapshot());
367 let status_code = if status.ready {
368 StatusCode::Ok
369 } else {
370 StatusCode::ServiceUnavailable
371 };
372 json_response(status_code, &status)
373 }
374 ("GET", METRICS_PATH) => Ok(response(
375 StatusCode::Ok,
376 Some(metrics_route::CONTENT_TYPE),
377 metrics_route::render_body().as_bytes(),
378 )),
379 ("GET", UNLOADABLE_PATH) => json_response(StatusCode::Ok, &served.unloadable.status()),
383 (_, HEALTH_PATH | READY_PATH | METRICS_PATH | UNLOADABLE_PATH) => {
384 Ok(empty_response(StatusCode::MethodNotAllowed))
385 }
386 _ => Ok(empty_response(StatusCode::NotFound)),
387 }
388}
389
390fn parse_request_line(request: &str) -> Option<(&str, &str)> {
391 let request_line = request.lines().next()?;
392 let mut parts = request_line.split_whitespace();
393 let method = parts.next()?;
394 let path = parts.next()?;
395 parts.next()?;
396
397 Some((method, path))
398}
399
400fn json_response<T>(status: StatusCode, value: &T) -> Result<Vec<u8>, ServerError>
401where
402 T: serde::Serialize,
403{
404 let body = serde_json::to_vec(value).map_err(|error| ServerError::HealthEndpoint {
405 message: format!("failed to serialize health response: {error}"),
406 })?;
407 Ok(response(status, Some(APPLICATION_JSON), &body))
408}
409
410fn empty_response(status: StatusCode) -> Vec<u8> {
411 response(status, None, &[])
412}
413
414fn response(status: StatusCode, content_type: Option<&str>, body: &[u8]) -> Vec<u8> {
415 let mut response = Vec::new();
416 let status_line = format!("HTTP/1.1 {} {}\r\n", status.code(), status.reason());
417 response.extend_from_slice(status_line.as_bytes());
418 response.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
419 response.extend_from_slice(b"Connection: close\r\n");
420 if let Some(content_type) = content_type {
421 response.extend_from_slice(format!("Content-Type: {content_type}\r\n").as_bytes());
422 }
423 response.extend_from_slice(b"\r\n");
424 response.extend_from_slice(body);
425 response
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429enum StatusCode {
430 Ok,
431 BadRequest,
432 NotFound,
433 MethodNotAllowed,
434 ServiceUnavailable,
435}
436
437impl StatusCode {
438 const fn code(self) -> u16 {
439 match self {
440 Self::Ok => 200,
441 Self::BadRequest => 400,
442 Self::NotFound => 404,
443 Self::MethodNotAllowed => 405,
444 Self::ServiceUnavailable => 503,
445 }
446 }
447
448 const fn reason(self) -> &'static str {
449 match self {
450 Self::Ok => "OK",
451 Self::BadRequest => "Bad Request",
452 Self::NotFound => "Not Found",
453 Self::MethodNotAllowed => "Method Not Allowed",
454 Self::ServiceUnavailable => "Service Unavailable",
455 }
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use std::io::{Read, Write};
462 use std::net::{SocketAddr, TcpStream};
463 use std::thread;
464 use std::time::{Duration, Instant};
465
466 use serde_json::Value;
467
468 use super::{ServedState, response_for_request, start_health_server};
469 use crate::health::checks::{
470 ClusterReadiness, ReadinessCondition, ReadinessState, SharedReadinessState,
471 };
472
473 fn loopback_ephemeral() -> Result<SocketAddr, Box<dyn std::error::Error>> {
474 Ok("127.0.0.1:0".parse()?)
475 }
476
477 fn served(readiness: SharedReadinessState) -> ServedState {
481 ServedState {
482 readiness,
483 unloadable: crate::health::unloadable::SharedUnloadableConversations::default(),
484 }
485 }
486
487 fn get(address: SocketAddr, path: &str) -> Result<String, Box<dyn std::error::Error>> {
488 let mut stream = TcpStream::connect(address)?;
489 stream.set_read_timeout(Some(Duration::from_secs(2)))?;
490 let request = format!("GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n");
491 stream.write_all(request.as_bytes())?;
492
493 let mut response = String::new();
494 stream.read_to_string(&mut response)?;
495 Ok(response)
496 }
497
498 fn assert_status(response: &str, status: u16) {
499 let expected = format!("HTTP/1.1 {status} ");
500 assert!(
501 response.starts_with(&expected),
502 "response status did not start with {expected}: {response}"
503 );
504 }
505
506 fn body(response: &str) -> Result<&str, Box<dyn std::error::Error>> {
507 let Some((_headers, body)) = response.split_once("\r\n\r\n") else {
508 return Err("response did not contain a header/body separator".into());
509 };
510 Ok(body)
511 }
512
513 fn json_body(response: &str) -> Result<Value, Box<dyn std::error::Error>> {
514 Ok(serde_json::from_str(body(response)?)?)
515 }
516
517 #[test]
518 fn health_endpoint_returns_json_200_regardless_of_readiness()
519 -> Result<(), Box<dyn std::error::Error>> {
520 let readiness = SharedReadinessState::new(ReadinessState::default());
521 let server = start_health_server(loopback_ephemeral()?, readiness)?;
522
523 let response = get(server.local_addr(), "/health")?;
524 server.shutdown()?;
525
526 assert_status(&response, 200);
527 assert!(response.contains("Content-Type: application/json\r\n"));
528 let body = json_body(&response)?;
529 assert_eq!(body["status"], "healthy");
530
531 Ok(())
532 }
533
534 #[test]
535 fn ready_endpoint_returns_503_before_main_listener_binds()
536 -> Result<(), Box<dyn std::error::Error>> {
537 let readiness = SharedReadinessState::new(ReadinessState::new(
538 true,
539 false,
540 ClusterReadiness::NotConfigured,
541 ));
542 let server = start_health_server(loopback_ephemeral()?, readiness)?;
543
544 let response = get(server.local_addr(), "/ready")?;
545 server.shutdown()?;
546
547 assert_status(&response, 503);
548 assert!(response.contains("Content-Type: application/json\r\n"));
549 let body = json_body(&response)?;
550 assert_eq!(body["ready"], false);
551 assert_eq!(body["unmet_conditions"][0], "listener_bound");
552
553 Ok(())
554 }
555
556 #[test]
557 fn ready_endpoint_returns_200_after_all_startup_gates() -> Result<(), Box<dyn std::error::Error>>
558 {
559 let readiness = SharedReadinessState::new(ReadinessState::ready_without_cluster());
560 let server = start_health_server(loopback_ephemeral()?, readiness)?;
561
562 let response = get(server.local_addr(), "/ready")?;
563 server.shutdown()?;
564
565 assert_status(&response, 200);
566 let body = json_body(&response)?;
567 assert_eq!(body["ready"], true);
568 let Some(unmet_conditions) = body["unmet_conditions"].as_array() else {
569 return Err("unmet_conditions should be an array".into());
570 };
571 assert!(unmet_conditions.is_empty());
572
573 Ok(())
574 }
575
576 #[test]
577 fn ready_endpoint_updates_from_shared_readiness_state() -> Result<(), Box<dyn std::error::Error>>
578 {
579 let readiness = SharedReadinessState::new(ReadinessState::default());
580 let server = start_health_server(loopback_ephemeral()?, readiness.clone())?;
581
582 let response = get(server.local_addr(), "/ready")?;
583 assert_status(&response, 503);
584
585 readiness.set_config_loaded(true);
586 readiness.set_listener_bound(true);
587 let response = get(server.local_addr(), "/ready")?;
588 server.shutdown()?;
589
590 assert_status(&response, 200);
591
592 Ok(())
593 }
594
595 #[test]
596 fn clustered_ready_transitions_503_to_200_when_membership_established()
597 -> Result<(), Box<dyn std::error::Error>> {
598 let readiness = SharedReadinessState::new(ReadinessState::new(
601 true,
602 true,
603 ClusterReadiness::Configured {
604 membership_established: false,
605 },
606 ));
607 let server = start_health_server(loopback_ephemeral()?, readiness.clone())?;
608
609 let response = get(server.local_addr(), "/ready")?;
610 assert_status(&response, 503);
611 let body = json_body(&response)?;
612 assert_eq!(body["ready"], false);
613 assert_eq!(
614 body["unmet_conditions"][0],
615 serde_json::to_value(ReadinessCondition::ClusterMembershipEstablished)?
616 );
617
618 readiness.set_cluster_membership_established(true);
621 let response = get(server.local_addr(), "/ready")?;
622 server.shutdown()?;
623
624 assert_status(&response, 200);
625 let body = json_body(&response)?;
626 assert_eq!(body["ready"], true);
627 let Some(unmet_conditions) = body["unmet_conditions"].as_array() else {
628 return Err("unmet_conditions should be an array".into());
629 };
630 assert!(unmet_conditions.is_empty());
631
632 Ok(())
633 }
634
635 #[test]
636 fn cluster_readiness_is_listed_when_configured_but_not_joined()
637 -> Result<(), Box<dyn std::error::Error>> {
638 let readiness = SharedReadinessState::new(ReadinessState::new(
639 true,
640 true,
641 ClusterReadiness::Configured {
642 membership_established: false,
643 },
644 ));
645 let response = response_for_request(b"GET /ready HTTP/1.1\r\n\r\n", &served(readiness))?;
646 let response = String::from_utf8(response)?;
647
648 assert_status(&response, 503);
649 let body = json_body(&response)?;
650 assert_eq!(
651 body["unmet_conditions"][0],
652 serde_json::to_value(ReadinessCondition::ClusterMembershipEstablished)?
653 );
654
655 Ok(())
656 }
657
658 #[test]
676 fn unloadable_conversations_route_answers_the_operator_a_json_shape()
677 -> Result<(), Box<dyn std::error::Error>> {
678 let readiness = SharedReadinessState::new(ReadinessState::default());
679 let server = start_health_server(loopback_ephemeral()?, readiness)?;
680
681 let response = get(server.local_addr(), "/unloadable-conversations")?;
682 let unknown = get(server.local_addr(), "/unloadable-conversations-typo")?;
683 server.shutdown()?;
684
685 assert_status(&unknown, 404);
688
689 assert_status(&response, 200);
690 assert!(
691 response.contains("Content-Type: application/json\r\n"),
692 "the unloadable-conversations route must answer JSON: {response}"
693 );
694 let body = json_body(&response)?;
695 assert_eq!(
696 body["count"], 0,
697 "a server with no participant record attached refuses nothing: {body}"
698 );
699 assert_eq!(
700 body["participant_installed"], false,
701 "no participant record is attached to this server, and the surface must say so \
702 rather than let a zero count read as a clean node: {body}"
703 );
704 let Some(conversations) = body["conversations"].as_array() else {
705 return Err("conversations should be an array".into());
706 };
707 assert!(
708 conversations.is_empty(),
709 "no conversation was refused: {conversations:?}"
710 );
711
712 Ok(())
713 }
714
715 #[test]
716 fn unsupported_paths_are_not_served() -> Result<(), Box<dyn std::error::Error>> {
717 let readiness = SharedReadinessState::default();
718 let response = response_for_request(b"GET /unknown HTTP/1.1\r\n\r\n", &served(readiness))?;
719 let response = String::from_utf8(response)?;
720
721 assert_status(&response, 404);
722
723 Ok(())
724 }
725
726 #[test]
727 fn unsupported_methods_on_health_paths_are_rejected() -> Result<(), Box<dyn std::error::Error>>
728 {
729 let readiness = SharedReadinessState::default();
730 let response = response_for_request(b"POST /health HTTP/1.1\r\n\r\n", &served(readiness))?;
731 let response = String::from_utf8(response)?;
732
733 assert_status(&response, 405);
734
735 Ok(())
736 }
737
738 #[test]
743 fn silent_health_listener_has_zero_application_wakes() -> Result<(), Box<dyn std::error::Error>>
744 {
745 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
746
747 let deadline = Instant::now() + Duration::from_secs(2);
748 while server.accept_attempts() < 1 && Instant::now() < deadline {
749 thread::sleep(Duration::from_millis(5));
750 }
751 let armed = server.accept_attempts();
752 assert_eq!(
753 armed, 1,
754 "the blocking accept is issued exactly once when parked"
755 );
756
757 thread::sleep(Duration::from_millis(200));
758 assert_eq!(
759 server.accept_attempts(),
760 armed,
761 "a silent health listener must not wake or re-accept"
762 );
763 assert_eq!(
764 server.shed_count(),
765 0,
766 "a silent health listener sheds nothing"
767 );
768
769 let response = get(server.local_addr(), "/health")?;
771 assert_status(&response, 200);
772 let body = json_body(&response)?;
773 assert_eq!(body["status"], "healthy");
774
775 server.shutdown()?;
776 Ok(())
777 }
778
779 #[test]
783 fn health_accept_source_has_no_wouldblock_sleep_poll() {
784 const SOURCE: &str = include_str!("endpoint.rs");
785 let production = SOURCE.split("mod tests").next().unwrap_or(SOURCE);
786 for forbidden in [
787 "set_nonblocking(true)",
788 "ErrorKind::WouldBlock",
789 "thread::sleep",
790 ] {
791 assert!(
792 !production.contains(forbidden),
793 "retired health accept-path source `{forbidden}` reappeared"
794 );
795 }
796 }
797
798 #[test]
804 fn health_shutdown_interrupts_accept_wait() -> Result<(), Box<dyn std::error::Error>> {
805 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
807 let start = Instant::now();
808 server.shutdown()?;
809 assert!(
810 start.elapsed() < Duration::from_secs(2),
811 "shutdown before arming must interrupt promptly, not sleep-poll"
812 );
813
814 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
816 let deadline = Instant::now() + Duration::from_secs(2);
817 while server.accept_attempts() < 1 && Instant::now() < deadline {
818 thread::sleep(Duration::from_millis(5));
819 }
820 assert_eq!(
821 server.accept_attempts(),
822 1,
823 "the worker parked before shutdown"
824 );
825 let parked_addr = server.local_addr();
826 let start = Instant::now();
827 server.shutdown()?;
828 assert!(
829 start.elapsed() < Duration::from_secs(2),
830 "shutdown of a parked accept must interrupt promptly"
831 );
832 assert!(
834 TcpStream::connect(parked_addr).is_err(),
835 "the listener descriptor was released; further connects are refused"
836 );
837
838 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
848 let deadline = Instant::now() + Duration::from_secs(2);
849 while server.accept_attempts() < 1 && Instant::now() < deadline {
850 thread::sleep(Duration::from_millis(5));
851 }
852 let _pending = TcpStream::connect(server.local_addr())?;
853 let start = Instant::now();
854 server.shutdown()?;
855 assert!(
856 start.elapsed() < Duration::from_millis(500),
857 "shutdown concurrent with a pending accept must interrupt promptly (TOLD), \
858 not defer by a request read deadline: elapsed {:?}",
859 start.elapsed()
860 );
861
862 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
864 let response = get(server.local_addr(), "/health")?;
865 assert_status(&response, 200);
866 let start = Instant::now();
867 server.shutdown()?;
868 assert!(
869 start.elapsed() < Duration::from_secs(2),
870 "shutdown after a served request must interrupt the next parked accept promptly"
871 );
872
873 Ok(())
874 }
875
876 #[test]
892 fn shutdown_interrupts_in_flight_silent_request_read() -> Result<(), Box<dyn std::error::Error>>
893 {
894 let server = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
895
896 let _silent = TcpStream::connect(server.local_addr())?;
901
902 let deadline = Instant::now() + Duration::from_secs(2);
905 while server.requests_entered() < 1 && Instant::now() < deadline {
906 thread::sleep(Duration::from_millis(5));
907 }
908 assert_eq!(
909 server.requests_entered(),
910 1,
911 "the worker entered the in-flight silent request read"
912 );
913
914 let start = Instant::now();
915 server.shutdown()?;
916 assert!(
917 start.elapsed() < Duration::from_millis(500),
918 "shutdown must interrupt an in-flight silent request read promptly (TOLD), \
919 not defer by the read's admitted 2s deadline: elapsed {:?}",
920 start.elapsed()
921 );
922
923 Ok(())
924 }
925
926 #[test]
933 fn health_idle_grows_unrelated_counters_while_accept_stays_flat()
934 -> Result<(), Box<dyn std::error::Error>> {
935 let idle = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
936 let busy = start_health_server(loopback_ephemeral()?, SharedReadinessState::default())?;
937
938 let deadline = Instant::now() + Duration::from_secs(2);
939 while (idle.accept_attempts() < 1 || busy.accept_attempts() < 1)
940 && Instant::now() < deadline
941 {
942 thread::sleep(Duration::from_millis(5));
943 }
944 let idle_armed = idle.accept_attempts();
945 assert_eq!(idle_armed, 1, "the idle listener parks exactly one accept");
946 let busy_before = busy.accept_attempts();
947
948 for _ in 0..5 {
951 let response = get(busy.local_addr(), "/health")?;
952 assert_status(&response, 200);
953 }
954 let deadline = Instant::now() + Duration::from_secs(2);
955 while busy.accept_attempts() <= busy_before && Instant::now() < deadline {
956 thread::sleep(Duration::from_millis(5));
957 }
958
959 assert!(
960 busy.accept_attempts() > busy_before,
961 "an unrelated served request grows the busy listener's accept counter"
962 );
963 assert_eq!(
964 idle.accept_attempts(),
965 idle_armed,
966 "the silent listener's accept counter stays flat during the workload"
967 );
968 assert_eq!(idle.shed_count(), 0, "the silent listener sheds nothing");
969
970 idle.shutdown()?;
971 busy.shutdown()?;
972 Ok(())
973 }
974}