slice-5: readyz timeout covers send; fix test thread leak
Task 5 review fixes: the 250ms readiness bound now wraps the whole Stats round trip (a wedged thread can stall the send, not just the reply), and the readyz integration test tears down its MediaThread like the sibling test does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012QndwfhjyTiZcUYp87dwW8 Signed-off-by: Aaron D. Lee <himself@adlee.work>
This commit is contained in:
@@ -17,7 +17,6 @@ use axum::{Json, Router};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::media_thread::MediaCmd;
|
|
||||||
use crate::session_map::AppState;
|
use crate::session_map::AppState;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -168,11 +167,22 @@ pub async fn healthz() -> Response {
|
|||||||
/// the zombie-node failure from the 2026-07-04 review).
|
/// the zombie-node failure from the 2026-07-04 review).
|
||||||
pub async fn readyz(State(state): State<AppState>) -> Response {
|
pub async fn readyz(State(state): State<AppState>) -> Response {
|
||||||
let (reply, rx) = tokio::sync::oneshot::channel();
|
let (reply, rx) = tokio::sync::oneshot::channel();
|
||||||
if state.cmd_tx.send(MediaCmd::Stats { reply }).await.is_err() {
|
// One timeout bounds the WHOLE round trip — send AND reply. A wedged
|
||||||
return (StatusCode::SERVICE_UNAVAILABLE, "media thread unresponsive").into_response();
|
// media thread can stall the send (full command channel), not just
|
||||||
}
|
// the reply, and the probe's 250ms promise must hold in that case too.
|
||||||
let stats = match tokio::time::timeout(std::time::Duration::from_millis(250), rx).await {
|
let stats = match tokio::time::timeout(std::time::Duration::from_millis(250), async {
|
||||||
Ok(Ok(s)) => s,
|
state
|
||||||
|
.cmd_tx
|
||||||
|
.send(crate::media_thread::MediaCmd::Stats { reply })
|
||||||
|
.await
|
||||||
|
.ok()?;
|
||||||
|
rx.await.ok()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(s)) => s,
|
||||||
|
// Elapsed timeout, send failure (thread gone), or dropped reply —
|
||||||
|
// all mean the media thread cannot vouch for itself right now.
|
||||||
_ => {
|
_ => {
|
||||||
return (StatusCode::SERVICE_UNAVAILABLE, "media thread unresponsive").into_response();
|
return (StatusCode::SERVICE_UNAVAILABLE, "media thread unresponsive").into_response();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ async fn get_root_serves_html() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn readyz_200_with_stats_json_when_thread_alive() {
|
async fn readyz_200_with_stats_json_when_thread_alive() {
|
||||||
let state = rutster::session_map::AppState::default();
|
let state = rutster::session_map::AppState::default();
|
||||||
let (state, _thread) = state
|
let (state, media_thread) = state
|
||||||
.spawn_media_thread(tokio::runtime::Handle::current())
|
.spawn_media_thread(tokio::runtime::Handle::current())
|
||||||
.expect("spawn");
|
.expect("spawn");
|
||||||
let app = rutster::routes::router(state);
|
let app = rutster::routes::router(state);
|
||||||
@@ -127,4 +127,7 @@ async fn readyz_200_with_stats_json_when_thread_alive() {
|
|||||||
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
|
||||||
assert_eq!(v["draining"], false);
|
assert_eq!(v["draining"], false);
|
||||||
assert!(v["max_sessions"].as_u64().unwrap() > 0);
|
assert!(v["max_sessions"].as_u64().unwrap() > 0);
|
||||||
|
tokio::task::spawn_blocking(move || media_thread.shutdown())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user