Skip to main content

liminal_server/health/
endpoint.rs

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/// Handle for a running health endpoint server.
24///
25/// W4 leg 2 (§4.2): the health accept worker BLOCKS in `accept` (kernel-parked,
26/// zero idle wakes) rather than spinning a non-blocking poll with a backoff
27/// sleep. Shutdown wakes the blocked `accept` with an explicit self-connect
28/// interrupt to `interrupt_target` (the bound address, loopback-normalised),
29/// mirroring the leg-1 listeners. Shutdown also interrupts an in-flight request
30/// read directly via `active_stream`, so a silent client parked in
31/// `handle_connection`'s read cannot defer shutdown by its admitted deadline.
32#[derive(Debug)]
33pub struct HealthServerHandle {
34    local_addr: SocketAddr,
35    /// Loopback-normalised self-connect target used to interrupt the blocking
36    /// `accept` at shutdown.
37    interrupt_target: SocketAddr,
38    shutdown: Arc<AtomicBool>,
39    /// Slot holding a `try_clone` of the request stream the worker is currently
40    /// reading, if any. The worker registers it under the lock (with a shutdown
41    /// recheck) before blocking on the request read and clears it on completion;
42    /// `stop_worker` takes it and `shutdown(Both)`s it to interrupt an in-flight
43    /// blocking read directly (TOLD) rather than waiting out the read's admitted
44    /// deadline. At most one stream exists at a time (the worker is serial), so
45    /// one slot suffices.
46    active_stream: Arc<Mutex<Option<TcpStream>>>,
47    /// The operator read surface for refused conversation loads, shared with
48    /// the worker. The health server binds before any participant handler
49    /// exists, so this starts empty and the participant's record is published
50    /// into it by [`Self::install_unloadable_record`] once built. Pull-only:
51    /// nothing reads it until a request arrives, so it cannot wake the worker.
52    unloadable: SharedUnloadableConversations,
53    worker: Option<JoinHandle<Result<(), ServerError>>>,
54    /// Count of `accept` calls issued by the worker (test observability for the
55    /// zero-idle-wakes oracle: on a silent listener this stays at the single
56    /// parked call). The worker always maintains the counter; only the host-side
57    /// handle for reading it is test-scoped.
58    #[cfg(test)]
59    accept_attempts: Arc<AtomicU64>,
60    /// Count of connections shed under fd exhaustion via the reserve descriptor
61    /// (test observability for the shed helper reused from leg 1).
62    #[cfg(test)]
63    shed_count: Arc<AtomicU64>,
64    /// Count of accepted requests the worker has entered (incremented just
65    /// before it blocks reading the request). Test observability so a race can
66    /// deterministically catch the worker mid-request rather than parked in
67    /// `accept`.
68    #[cfg(test)]
69    requests_entered: Arc<AtomicU64>,
70}
71
72impl HealthServerHandle {
73    /// Returns the bound address for the health endpoint server.
74    #[must_use]
75    pub const fn local_addr(&self) -> SocketAddr {
76        self.local_addr
77    }
78
79    /// Publishes a participant's unloadable-conversation record onto
80    /// `GET /unloadable-conversations`.
81    ///
82    /// Server startup binds the health endpoint FIRST — liveness has to be
83    /// answerable while the rest of the server is still being built — so the
84    /// record cannot be a constructor argument. Until this is called the route
85    /// answers `participant_installed: false`, which is a different answer from
86    /// "nothing is refused" and is reported as such.
87    ///
88    /// Installing shares the handler's live record rather than copying it: a
89    /// refusal recorded after this call is reported by the next scrape. No
90    /// thread, timer, or notification is created here.
91    pub fn install_unloadable_record(&self, record: UnloadableConversationRecord) {
92        self.unloadable.install(record);
93    }
94
95    /// Stops the health endpoint server and waits for its worker thread to exit.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`ServerError::HealthEndpoint`] if the worker thread cannot be
100    /// joined cleanly or if the server loop recorded a serving error.
101    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        // Interrupt an in-flight request read directly (TOLD): the flag store
111        // above happens-before this lock acquire, so any stream the worker
112        // registered before observing the flag is taken here and shut down,
113        // waking its blocked read at once. `shutdown(Both)` on the clone reaches
114        // the same underlying socket the worker is reading (a dup'd fd). If the
115        // worker is instead parked in `accept`, the slot is empty and the
116        // self-connect below wakes it. The guard is released (statement end)
117        // before the socket call, so no lock is held across `shutdown`.
118        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        // Explicit cross-platform interrupt (mirrors the leg-1 listeners): a
127        // single self-connect wakes a blocked `accept`. The worker sees the
128        // shutdown flag it observed under the same ordering and sheds the woken
129        // (spurious) socket rather than serving it — at most one spurious accept
130        // per interrupt. If the worker already exited (a real accept then flag
131        // check), the listener is gone and this connect fails fast; the join then
132        // returns immediately.
133        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    /// `accept` calls issued by the worker (test observability, oracle 8).
143    #[cfg(test)]
144    fn accept_attempts(&self) -> u64 {
145        self.accept_attempts.load(Ordering::SeqCst)
146    }
147
148    /// Connections shed under fd exhaustion (test observability).
149    #[cfg(test)]
150    fn shed_count(&self) -> u64 {
151        self.shed_count.load(Ordering::SeqCst)
152    }
153
154    /// Accepted requests the worker has entered (test observability).
155    #[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
169/// Starts the health endpoint HTTP server on a distinct health bind address.
170///
171/// The returned server handle is independent from the main wire protocol
172/// listener. Binding the health endpoint does not mark the main listener ready.
173///
174/// # Errors
175///
176/// Returns [`ServerError::HealthEndpoint`] when the health listener cannot bind
177/// or cannot report its local address. The listener stays BLOCKING (W4 leg 2):
178/// the accept worker kernel-parks in `accept` with zero idle wakes; shutdown
179/// wakes it via the self-connect interrupt.
180pub 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    // The listener stays BLOCKING (W4 leg 2): the accept worker kernel-parks in
189    // `accept` with zero idle wakes; shutdown wakes it via the self-connect
190    // interrupt below.
191    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/// Everything a request is answered from, carried as one value so the worker's
241/// parameter list does not grow a slot per operator surface.
242#[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    // One reserve descriptor held for the shed-with-spare-fd EMFILE policy,
258    // reusing the leg-1 helper.
259    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                // Register the in-flight stream and re-check shutdown atomically
265                // under the slot lock. If shutdown already fired, shed without
266                // serving (no request slips past the broadcast); otherwise the
267                // registered clone lets `stop_worker` interrupt this request's
268                // blocking read directly (TOLD). Pairing the flag load here with
269                // `stop_worker`'s flag-store-then-lock means a registration can
270                // never be missed: either the worker sees the flag and sheds, or
271                // shutdown finds the clone in the slot.
272                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                // A per-connection error (e.g. a TCP probe that connects but sends no HTTP
287                // data within the read timeout) must NOT terminate the serve loop — otherwise
288                // a single port probe kills the health server for the process lifetime and
289                // subsequent liveness/readiness probes get connection-refused. Only fatal
290                // listener-level accept errors (below) terminate serving.
291                let result = handle_connection(stream, served);
292                // The request is done: clear the slot so a later shutdown finds
293                // nothing to interrupt (and never shuts down an unrelated stream).
294                // A no-op if `stop_worker` already took the clone.
295                *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
315/// EMFILE/ENFILE resource exhaustion is transient, exactly as the leg-1 accept
316/// loops treat it (mirrors the sibling WebSocket listener's local predicate).
317fn 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        // The refused-load surface. Reading it is a snapshot of a record
380        // another part of the server already maintains — this route computes
381        // nothing, schedules nothing, and touches no conversation.
382        ("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    /// The request-level tests answer from readiness alone; the refused-load
478    /// surface stays uninstalled, which is the state a server is in before its
479    /// participant handler exists.
480    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        // A clustered server starts with the cluster gate unmet: config loaded and
599        // listener bound, but membership not yet established (G2). /ready is 503.
600        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        // The cluster start's on_established hook flips exactly this flag; once set,
619        // /ready must transition to 200 with no unmet conditions.
620        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    /// THE UNLOADABLE-CONVERSATIONS READ SURFACE, RED UNTIL THE ROUTE EXISTS.
659    ///
660    /// The node already records every conversation it refused to load
661    /// (`server/participant/production/handler.rs`, `record_unloadable`) and
662    /// then offers an operator no way to read it back: the accessor has no
663    /// caller. Containment without a read surface is a node that serves nothing
664    /// on one conversation forever and answers no question about it.
665    ///
666    /// This pin fixes the shape of the answer and nothing else — that the route
667    /// EXISTS, answers JSON 200, and carries the three fields an operator reads:
668    /// how many conversations are refused, which ones, and whether a participant
669    /// record is even attached (so a `count` of zero is never confused with a
670    /// node that has no participant installed at all).
671    ///
672    /// The unknown-path arm in the same test is the discriminator: without it a
673    /// 200 here would also be satisfied by a server that answers 200 to
674    /// everything.
675    #[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        // The discriminator first: a neighbouring path is still not served, so
686        // the 200 below is this route's own answer.
687        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    /// Oracle 8 (W4 leg 2) — on a quiet health listener the blocking accept is
739    /// issued exactly once (the parked call) and never again: zero repeated
740    /// accepts, zero application wakes after arming, with route behaviour
741    /// unchanged (a real request is still served afterwards).
742    #[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        // Route behaviour unchanged: a real request is still served after silence.
770        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    /// Oracle 9 (W4 leg 2) — absence proof over this module's production source:
780    /// the retired non-blocking flip and its `WouldBlock` + sleep poll must not
781    /// appear in the health accept path.
782    #[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    /// Oracle 10 (W4 leg 2) — shutdown interrupts the blocking accept wait at
799    /// every race point: before the worker arms, after it parks, concurrent with
800    /// a pending connection, and after an accept returns. Each shutdown returns
801    /// promptly (no sleep-poll) and joins cleanly (no worker leak); the released
802    /// listener refuses further connects (no descriptor leak).
803    #[test]
804    fn health_shutdown_interrupts_accept_wait() -> Result<(), Box<dyn std::error::Error>> {
805        // (a) shutdown immediately after start — possibly before the worker arms.
806        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        // (b) shutdown after the worker has parked the blocking accept.
815        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        // No descriptor leak: the released listener refuses further connects.
833        assert!(
834            TcpStream::connect(parked_addr).is_err(),
835            "the listener descriptor was released; further connects are refused"
836        );
837
838        // (c) shutdown concurrent with accept readiness: a client is pending.
839        // Whether the worker is still parked in `accept` or has already accepted
840        // and is blocked reading the silent client, shutdown interrupts promptly
841        // (self-connect for the parked case, in-flight stream shutdown for the
842        // reading case) — deterministically under the 2s read deadline. The bound
843        // is tightened to 500 ms to reflect the TOLD interrupt: it must not
844        // approach the read window that the pre-fix bytes deferred to (see
845        // `shutdown_interrupts_in_flight_silent_request_read` for the dedicated
846        // mid-read regression).
847        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        // (d) shutdown after an accept returns and a request is served.
863        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    /// Regression for oracle 10 case (c) (W4 leg 2 — tear-seat BOUNCE): an
877    /// in-flight SILENT request must not defer shutdown by its read window. A
878    /// client that connects and sends nothing parks the worker inside
879    /// `handle_connection`'s blocking read (the admitted 2s slow-client
880    /// deadline). Shutdown must interrupt that read directly (TOLD stream
881    /// interrupt), NOT wait for the deadline to expire.
882    ///
883    /// The promptness bound is inherently a timing assertion: it is set to
884    /// 500 ms — comfortably under the 2s read deadline (so the pre-fix bytes,
885    /// where the self-connect interrupt merely queues behind the blocked read,
886    /// red deterministically) and comfortably over scheduler-wakeup noise even
887    /// under full-workspace parallel load (so the post-fix stream interrupt
888    /// greens deterministically). The worker's entry into the read is observed
889    /// via the `requests_entered` counter, not a sleep, so the race is caught
890    /// deterministically mid-request.
891    #[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        // Connect but send NOTHING and keep the socket open: the worker accepts
897        // and blocks in handle_connection's read on the silent stream. The named
898        // binding keeps the client alive (a bare `_` would drop it and end the
899        // read early with EOF).
900        let _silent = TcpStream::connect(server.local_addr())?;
901
902        // Observe the worker has entered the in-flight request read (counter, not
903        // a sleep) so shutdown is fired while it is genuinely blocked on the read.
904        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    /// Oracle 11 (W4 leg 2, idle-honesty both-sides) — an unrelated served
927    /// request grows the BUSY listener's accept-attempt counter while the silent
928    /// listener's accept-attempt counter stays FLAT during the workload. The
929    /// growing side proves the fixture cannot pass by hiding the workload (a
930    /// frozen harness would leave the busy counter flat and fail); the flat side
931    /// proves genuine silence rather than a global freeze.
932    #[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        // Unrelated served workload on the BUSY listener: each served request
949        // returns the parked accept and re-parks a fresh one, growing its counter.
950        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}