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::{
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";
26/// R18 amendment A7 (§0.18): the operator credential re-issue operation.
27const REISSUE_PATH: &str = "/operator/credential-reissue";
28const APPLICATION_JSON: &str = "application/json";
29const READ_BUFFER_BYTES: usize = 2048;
30
31/// Handle for a running health endpoint server.
32///
33/// W4 leg 2 (§4.2): the health accept worker BLOCKS in `accept` (kernel-parked,
34/// zero idle wakes) rather than spinning a non-blocking poll with a backoff
35/// sleep. Shutdown wakes the blocked `accept` with an explicit self-connect
36/// interrupt to `interrupt_target` (the bound address, loopback-normalised),
37/// mirroring the leg-1 listeners. Shutdown also interrupts an in-flight request
38/// read directly via `active_stream`, so a silent client parked in
39/// `handle_connection`'s read cannot defer shutdown by its admitted deadline.
40#[derive(Debug)]
41pub struct HealthServerHandle {
42    local_addr: SocketAddr,
43    /// Loopback-normalised self-connect target used to interrupt the blocking
44    /// `accept` at shutdown.
45    interrupt_target: SocketAddr,
46    shutdown: Arc<AtomicBool>,
47    /// Slot holding a `try_clone` of the request stream the worker is currently
48    /// reading, if any. The worker registers it under the lock (with a shutdown
49    /// recheck) before blocking on the request read and clears it on completion;
50    /// `stop_worker` takes it and `shutdown(Both)`s it to interrupt an in-flight
51    /// blocking read directly (TOLD) rather than waiting out the read's admitted
52    /// deadline. At most one stream exists at a time (the worker is serial), so
53    /// one slot suffices.
54    active_stream: Arc<Mutex<Option<TcpStream>>>,
55    /// The operator read surface for refused conversation loads, shared with
56    /// the worker. The health server binds before any participant handler
57    /// exists, so this starts empty and the participant's record is published
58    /// into it by [`Self::install_unloadable_record`] once built. Pull-only:
59    /// nothing reads it until a request arrives, so it cannot wake the worker.
60    unloadable: SharedUnloadableConversations,
61    /// The operator WRITE surface for A7 credential re-issue, shared with the
62    /// worker. Empty until the participant is built, exactly like `unloadable`
63    /// above and for the same reason. Call-driven only: nothing reads it until
64    /// a request arrives, so it cannot wake the worker.
65    reissue: SharedOperatorCredentialReissue,
66    worker: Option<JoinHandle<Result<(), ServerError>>>,
67    /// Count of `accept` calls issued by the worker (test observability for the
68    /// zero-idle-wakes oracle: on a silent listener this stays at the single
69    /// parked call). The worker always maintains the counter; only the host-side
70    /// handle for reading it is test-scoped.
71    #[cfg(test)]
72    accept_attempts: Arc<AtomicU64>,
73    /// Count of connections shed under fd exhaustion via the reserve descriptor
74    /// (test observability for the shed helper reused from leg 1).
75    #[cfg(test)]
76    shed_count: Arc<AtomicU64>,
77    /// Count of accepted requests the worker has entered (incremented just
78    /// before it blocks reading the request). Test observability so a race can
79    /// deterministically catch the worker mid-request rather than parked in
80    /// `accept`.
81    #[cfg(test)]
82    requests_entered: Arc<AtomicU64>,
83}
84
85impl HealthServerHandle {
86    /// Returns the bound address for the health endpoint server.
87    #[must_use]
88    pub const fn local_addr(&self) -> SocketAddr {
89        self.local_addr
90    }
91
92    /// Publishes a participant's unloadable-conversation record onto
93    /// `GET /unloadable-conversations`.
94    ///
95    /// Server startup binds the health endpoint FIRST — liveness has to be
96    /// answerable while the rest of the server is still being built — so the
97    /// record cannot be a constructor argument. Until this is called the route
98    /// answers `participant_installed: false`, which is a different answer from
99    /// "nothing is refused" and is reported as such.
100    ///
101    /// Installing shares the handler's live record rather than copying it: a
102    /// refusal recorded after this call is reported by the next scrape. No
103    /// thread, timer, or notification is created here.
104    pub fn install_unloadable_record(&self, record: UnloadableConversationRecord) {
105        self.unloadable.install(record);
106    }
107
108    /// Publishes the participant authority onto
109    /// `POST /operator/credential-reissue` (R18 amendment A7, §0.18).
110    ///
111    /// Same timing constraint as [`Self::install_unloadable_record`]: the
112    /// health endpoint binds before the participant exists, so the authority
113    /// arrives here once built. Until it does the route answers 503 and says
114    /// no participant is installed, which is a different answer from an
115    /// unknown identity and is reported as such. No thread, timer, or
116    /// notification is created here.
117    pub fn install_credential_reissuer(&self, reissuer: Arc<dyn OperatorCredentialReissuer>) {
118        self.reissue.install(reissuer);
119    }
120
121    /// Stops the health endpoint server and waits for its worker thread to exit.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`ServerError::HealthEndpoint`] if the worker thread cannot be
126    /// joined cleanly or if the server loop recorded a serving error.
127    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        // Interrupt an in-flight request read directly (TOLD): the flag store
137        // above happens-before this lock acquire, so any stream the worker
138        // registered before observing the flag is taken here and shut down,
139        // waking its blocked read at once. `shutdown(Both)` on the clone reaches
140        // the same underlying socket the worker is reading (a dup'd fd). If the
141        // worker is instead parked in `accept`, the slot is empty and the
142        // self-connect below wakes it. The guard is released (statement end)
143        // before the socket call, so no lock is held across `shutdown`.
144        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        // Explicit cross-platform interrupt (mirrors the leg-1 listeners): a
153        // single self-connect wakes a blocked `accept`. The worker sees the
154        // shutdown flag it observed under the same ordering and sheds the woken
155        // (spurious) socket rather than serving it — at most one spurious accept
156        // per interrupt. If the worker already exited (a real accept then flag
157        // check), the listener is gone and this connect fails fast; the join then
158        // returns immediately.
159        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    /// `accept` calls issued by the worker (test observability, oracle 8).
169    #[cfg(test)]
170    fn accept_attempts(&self) -> u64 {
171        self.accept_attempts.load(Ordering::SeqCst)
172    }
173
174    /// Connections shed under fd exhaustion (test observability).
175    #[cfg(test)]
176    fn shed_count(&self) -> u64 {
177        self.shed_count.load(Ordering::SeqCst)
178    }
179
180    /// Accepted requests the worker has entered (test observability).
181    #[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
195/// Starts the health endpoint HTTP server on a distinct health bind address.
196///
197/// The returned server handle is independent from the main wire protocol
198/// listener. Binding the health endpoint does not mark the main listener ready.
199///
200/// # Errors
201///
202/// Returns [`ServerError::HealthEndpoint`] when the health listener cannot bind
203/// or cannot report its local address. The listener stays BLOCKING (W4 leg 2):
204/// the accept worker kernel-parks in `accept` with zero idle wakes; shutdown
205/// wakes it via the self-connect interrupt.
206pub 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    // The listener stays BLOCKING (W4 leg 2): the accept worker kernel-parks in
215    // `accept` with zero idle wakes; shutdown wakes it via the self-connect
216    // interrupt below.
217    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/// Everything a request is answered from, carried as one value so the worker's
270/// parameter list does not grow a slot per operator surface.
271#[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    // One reserve descriptor held for the shed-with-spare-fd EMFILE policy,
288    // reusing the leg-1 helper.
289    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                // Register the in-flight stream and re-check shutdown atomically
295                // under the slot lock. If shutdown already fired, shed without
296                // serving (no request slips past the broadcast); otherwise the
297                // registered clone lets `stop_worker` interrupt this request's
298                // blocking read directly (TOLD). Pairing the flag load here with
299                // `stop_worker`'s flag-store-then-lock means a registration can
300                // never be missed: either the worker sees the flag and sheds, or
301                // shutdown finds the clone in the slot.
302                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                // A `/metrics` body runs to many KB, so the response spans several
316                // segments and Nagle would hold its trailing partial segment for the
317                // scraper's delayed ACK. Same preparation as the leg-1 accept loops.
318                prepare_accepted_socket(&stream, None);
319                requests_entered.fetch_add(1, Ordering::SeqCst);
320                // A per-connection error (e.g. a TCP probe that connects but sends no HTTP
321                // data within the read timeout) must NOT terminate the serve loop — otherwise
322                // a single port probe kills the health server for the process lifetime and
323                // subsequent liveness/readiness probes get connection-refused. Only fatal
324                // listener-level accept errors (below) terminate serving.
325                let result = handle_connection(stream, served);
326                // The request is done: clear the slot so a later shutdown finds
327                // nothing to interrupt (and never shuts down an unrelated stream).
328                // A no-op if `stop_worker` already took the clone.
329                *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
349/// EMFILE/ENFILE resource exhaustion is transient, exactly as the leg-1 accept
350/// loops treat it (mirrors the sibling WebSocket listener's local predicate).
351fn 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        // The refused-load surface. Reading it is a snapshot of a record
414        // another part of the server already maintains — this route computes
415        // nothing, schedules nothing, and touches no conversation.
416        ("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        // R18 amendment A7. Matched on the path with its query string SPLIT
421        // OFF, and only here: the four routes above keep matching the whole
422        // request target exactly as they always have, so a query string on
423        // `/health` is still a 404 and no existing route's answer moves.
424        _ if path.split('?').next() == Some(REISSUE_PATH) => {
425            credential_reissue_response(method, path, served)
426        }
427        _ => Ok(empty_response(StatusCode::NotFound)),
428    }
429}
430
431/// Answers one `OperatorCredentialReissue` call (R18 amendment A7, §0.18).
432///
433/// # Why the inputs ride the query string
434///
435/// The three inputs are fixed by §0.18 (`conversation_id`, `participant_id`,
436/// `expected_current_generation`) and none of them is a secret — the SECRET
437/// travels only in the response. The request shape is a build decision, and a
438/// query string is chosen because this endpoint reads each request with ONE
439/// bounded read and no message framing: a body would be present only when the
440/// client happened to put it in the same segment, so parsing one would make
441/// the route's answer depend on TCP segmentation. Teaching the shared request
442/// reader to frame bodies would change the serving discipline of every route
443/// here, which is not this lane's to do.
444fn 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        // No participant is configured on this node. Distinct from every
458        // identity answer, and reported as such rather than as a lookup miss.
459        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            // The operation could not be DECIDED. The operator is told, and
468            // the text is the refusal's own — never a bare close.
469            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
478/// The status each typed refusal is served with.
479///
480/// A lookup miss is a 404 and every guard refusal is a 409: the identity
481/// resolved, and the operation was refused by a live fact about it. The
482/// discriminator an operator branches on is the body's `refusal` field, never
483/// the status — the status is the HTTP-shaped summary of it.
484const 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
496/// Parses the three §0.18 inputs, or answers `None` for anything else.
497///
498/// Every field is mandatory and every value is a plain decimal `u64`. An
499/// unknown parameter, a repeat, or a missing one is refused rather than
500/// defaulted: an operator who mistypes `expected_current_generation` must not
501/// have a zero silently substituted for it.
502fn 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    /// The request-level tests answer from readiness alone; the refused-load
621    /// surface stays uninstalled, which is the state a server is in before its
622    /// participant handler exists.
623    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        // A clustered server starts with the cluster gate unmet: config loaded and
743        // listener bound, but membership not yet established (G2). /ready is 503.
744        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        // The cluster start's on_established hook flips exactly this flag; once set,
763        // /ready must transition to 200 with no unmet conditions.
764        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    /// THE UNLOADABLE-CONVERSATIONS READ SURFACE, RED UNTIL THE ROUTE EXISTS.
803    ///
804    /// The node already records every conversation it refused to load
805    /// (`server/participant/production/handler.rs`, `record_unloadable`) and
806    /// then offers an operator no way to read it back: the accessor has no
807    /// caller. Containment without a read surface is a node that serves nothing
808    /// on one conversation forever and answers no question about it.
809    ///
810    /// This pin fixes the shape of the answer and nothing else — that the route
811    /// EXISTS, answers JSON 200, and carries the three fields an operator reads:
812    /// how many conversations are refused, which ones, and whether a participant
813    /// record is even attached (so a `count` of zero is never confused with a
814    /// node that has no participant installed at all).
815    ///
816    /// The unknown-path arm in the same test is the discriminator: without it a
817    /// 200 here would also be satisfied by a server that answers 200 to
818    /// everything.
819    #[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        // The discriminator first: a neighbouring path is still not served, so
830        // the 200 below is this route's own answer.
831        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    // -----------------------------------------------------------------------
883    // R18 amendment A7 (§0.18) — the operator credential re-issue route
884    // -----------------------------------------------------------------------
885
886    /// A reissuer that answers whatever it was built with, and records the
887    /// request it was handed so the route's parsing can be measured rather than
888    /// assumed.
889    #[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    /// A node with no participant configured says so, rather than answering
930    /// like a node whose identity is unknown.
931    #[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    /// A committed re-issue serves the secret ONCE, and the three §0.18 inputs
944    /// arrive at the authority exactly as the operator wrote them.
945    #[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    /// The NORMATIVE compare-and-set payload survives the route (§0.18 item 4),
984    /// and a guard refusal is a 409 whose discriminator is a field.
985    #[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    /// A pre-guard lookup miss is a 404, and discloses nothing beyond the
1011    /// identifiers the operator presented.
1012    #[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    /// A malformed call is REFUSED, never defaulted. An operator who mistypes a
1037    /// generation must not have a zero silently substituted for it and a
1038    /// credential rotated on the strength of it.
1039    #[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            // A missing input.
1049            "/operator/credential-reissue?conversation_id=7&participant_id=3",
1050            // No inputs at all.
1051            "/operator/credential-reissue",
1052            // A non-numeric generation.
1053            "/operator/credential-reissue?conversation_id=7&participant_id=3&\
1054             expected_current_generation=fourteen",
1055            // An unknown parameter riding along.
1056            "/operator/credential-reissue?conversation_id=7&participant_id=3&\
1057             expected_current_generation=14&force=1",
1058            // A repeated parameter, where the last one silently winning would be
1059            // an operator's typo deciding which identity rotates.
1060            "/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    /// The operation is a POST. A GET of the same target is refused rather than
1082    /// rotating a credential from a link someone clicked.
1083    #[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    /// ⛔ The query-string split must not have moved any EXISTING route's
1108    /// answer. `GET /health?x=1` was a 404 before A7 and stays one: the four
1109    /// original routes still match the whole request target exactly.
1110    #[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        // POSITIVE CONTROL: the same routes without a query string still serve,
1125        // so the assertions above measure the query handling and not a broken
1126        // request line.
1127        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    /// Oracle 8 (W4 leg 2) — on a quiet health listener the blocking accept is
1134    /// issued exactly once (the parked call) and never again: zero repeated
1135    /// accepts, zero application wakes after arming, with route behaviour
1136    /// unchanged (a real request is still served afterwards).
1137    #[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        // Route behaviour unchanged: a real request is still served after silence.
1165        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    /// Oracle 9 (W4 leg 2) — absence proof over this module's production source:
1175    /// the retired non-blocking flip and its `WouldBlock` + sleep poll must not
1176    /// appear in the health accept path.
1177    #[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    /// Oracle 10 (W4 leg 2) — shutdown interrupts the blocking accept wait at
1194    /// every race point: before the worker arms, after it parks, concurrent with
1195    /// a pending connection, and after an accept returns. Each shutdown returns
1196    /// promptly (no sleep-poll) and joins cleanly (no worker leak); the released
1197    /// listener refuses further connects (no descriptor leak).
1198    #[test]
1199    fn health_shutdown_interrupts_accept_wait() -> Result<(), Box<dyn std::error::Error>> {
1200        // (a) shutdown immediately after start — possibly before the worker arms.
1201        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        // (b) shutdown after the worker has parked the blocking accept.
1210        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        // No descriptor leak: the released listener refuses further connects.
1228        assert!(
1229            TcpStream::connect(parked_addr).is_err(),
1230            "the listener descriptor was released; further connects are refused"
1231        );
1232
1233        // (c) shutdown concurrent with accept readiness: a client is pending.
1234        // Whether the worker is still parked in `accept` or has already accepted
1235        // and is blocked reading the silent client, shutdown interrupts promptly
1236        // (self-connect for the parked case, in-flight stream shutdown for the
1237        // reading case) — deterministically under the 2s read deadline. The bound
1238        // is tightened to 500 ms to reflect the TOLD interrupt: it must not
1239        // approach the read window that the pre-fix bytes deferred to (see
1240        // `shutdown_interrupts_in_flight_silent_request_read` for the dedicated
1241        // mid-read regression).
1242        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        // (d) shutdown after an accept returns and a request is served.
1258        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    /// Regression for oracle 10 case (c) (W4 leg 2 — tear-seat BOUNCE): an
1272    /// in-flight SILENT request must not defer shutdown by its read window. A
1273    /// client that connects and sends nothing parks the worker inside
1274    /// `handle_connection`'s blocking read (the admitted 2s slow-client
1275    /// deadline). Shutdown must interrupt that read directly (TOLD stream
1276    /// interrupt), NOT wait for the deadline to expire.
1277    ///
1278    /// The promptness bound is inherently a timing assertion: it is set to
1279    /// 500 ms — comfortably under the 2s read deadline (so the pre-fix bytes,
1280    /// where the self-connect interrupt merely queues behind the blocked read,
1281    /// red deterministically) and comfortably over scheduler-wakeup noise even
1282    /// under full-workspace parallel load (so the post-fix stream interrupt
1283    /// greens deterministically). The worker's entry into the read is observed
1284    /// via the `requests_entered` counter, not a sleep, so the race is caught
1285    /// deterministically mid-request.
1286    #[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        // Connect but send NOTHING and keep the socket open: the worker accepts
1292        // and blocks in handle_connection's read on the silent stream. The named
1293        // binding keeps the client alive (a bare `_` would drop it and end the
1294        // read early with EOF).
1295        let _silent = TcpStream::connect(server.local_addr())?;
1296
1297        // Observe the worker has entered the in-flight request read (counter, not
1298        // a sleep) so shutdown is fired while it is genuinely blocked on the read.
1299        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    /// Oracle 11 (W4 leg 2, idle-honesty both-sides) — an unrelated served
1322    /// request grows the BUSY listener's accept-attempt counter while the silent
1323    /// listener's accept-attempt counter stays FLAT during the workload. The
1324    /// growing side proves the fixture cannot pass by hiding the workload (a
1325    /// frozen harness would leave the busy counter flat and fail); the flat side
1326    /// proves genuine silence rather than a global freeze.
1327    #[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        // Unrelated served workload on the BUSY listener: each served request
1344        // returns the parked accept and re-parks a fresh one, growing its counter.
1345        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}