web-ui: deadline-bound all broker-backed fetches so a busy hive-c0re can't hang api/state (closes #2148)
This commit is contained in:
parent
9496fb060b
commit
3d5957c87b
1 changed files with 49 additions and 25 deletions
|
|
@ -32,6 +32,14 @@ use crate::login::LoginState;
|
||||||
use crate::login_session::{LoginSession, drop_if_finished};
|
use crate::login_session::{LoginSession, drop_if_finished};
|
||||||
use crate::turn::TurnFiles;
|
use crate::turn::TurnFiles;
|
||||||
|
|
||||||
|
/// Deadline for broker-backed fetches on web-UI request paths. The
|
||||||
|
/// page's critical fields (status, turn state, usage) are all
|
||||||
|
/// in-memory; a busy or stalled hive-c0re must degrade the
|
||||||
|
/// socket-backed extras (inbox rows, loose ends, reminder stats)
|
||||||
|
/// instead of hanging the whole response — an unbounded await here is
|
||||||
|
/// what let `/api/state` stall long enough to bork the terminal.
|
||||||
|
const SOCKET_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
||||||
|
|
||||||
/// Live login state for the web UI. The harness updates this in place as it
|
/// Live login state for the web UI. The harness updates this in place as it
|
||||||
/// transitions between `NeedsLogin` and `Online`; the UI reads on each
|
/// transitions between `NeedsLogin` and `Online`; the UI reads on each
|
||||||
/// render.
|
/// render.
|
||||||
|
|
@ -512,18 +520,22 @@ struct SessionView {
|
||||||
/// the `mcp__hyperhive__get_loose_ends` tool sees from inside the
|
/// the `mcp__hyperhive__get_loose_ends` tool sees from inside the
|
||||||
/// container.
|
/// container.
|
||||||
async fn api_loose_ends(State(state): State<AppState>) -> Response {
|
async fn api_loose_ends(State(state): State<AppState>) -> Response {
|
||||||
let loose_ends: Vec<hive_sh4re::LooseEnd> = match client::request::<_, hive_sh4re::Response>(
|
let loose_ends: Vec<hive_sh4re::LooseEnd> = match tokio::time::timeout(
|
||||||
&state.socket,
|
SOCKET_FETCH_TIMEOUT,
|
||||||
&hive_sh4re::Request::GetLooseEnds { agent: None },
|
client::request::<_, hive_sh4re::Response>(
|
||||||
|
&state.socket,
|
||||||
|
&hive_sh4re::Request::GetLooseEnds { agent: None },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(hive_sh4re::Response::LooseEnds { loose_ends }) => loose_ends,
|
Ok(Ok(hive_sh4re::Response::LooseEnds { loose_ends })) => loose_ends,
|
||||||
Ok(hive_sh4re::Response::Err { message }) => {
|
Ok(Ok(hive_sh4re::Response::Err { message })) => {
|
||||||
return error_response(&format!("get_loose_ends: {message}"));
|
return error_response(&format!("get_loose_ends: {message}"));
|
||||||
}
|
}
|
||||||
Ok(other) => return error_response(&format!("unexpected response: {other:?}")),
|
Ok(Ok(other)) => return error_response(&format!("unexpected response: {other:?}")),
|
||||||
Err(e) => return error_response(&format!("transport: {e:#}")),
|
Ok(Err(e)) => return error_response(&format!("transport: {e:#}")),
|
||||||
|
Err(_) => return error_response("get_loose_ends: timed out — hive-c0re busy, retry"),
|
||||||
};
|
};
|
||||||
axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response()
|
axum::Json(serde_json::json!({ "loose_ends": loose_ends })).into_response()
|
||||||
}
|
}
|
||||||
|
|
@ -813,13 +825,18 @@ struct ExtraLink {
|
||||||
/// failure — the inbox section is decorative, not authoritative.
|
/// failure — the inbox section is decorative, not authoritative.
|
||||||
async fn recent_inbox(socket: &std::path::Path) -> Vec<hive_sh4re::InboxRow> {
|
async fn recent_inbox(socket: &std::path::Path) -> Vec<hive_sh4re::InboxRow> {
|
||||||
const LIMIT: u64 = 30;
|
const LIMIT: u64 = 30;
|
||||||
match client::request::<_, hive_sh4re::Response>(
|
// Deadline-bounded: `/api/state` must render even when hive-c0re is
|
||||||
socket,
|
// busy — an empty inbox section beats a hung snapshot.
|
||||||
&hive_sh4re::Request::Recent { limit: LIMIT },
|
match tokio::time::timeout(
|
||||||
|
SOCKET_FETCH_TIMEOUT,
|
||||||
|
client::request::<_, hive_sh4re::Response>(
|
||||||
|
socket,
|
||||||
|
&hive_sh4re::Request::Recent { limit: LIMIT },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(hive_sh4re::Response::Recent { rows }) => rows,
|
Ok(Ok(hive_sh4re::Response::Recent { rows })) => rows,
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -831,16 +848,19 @@ async fn fetch_reminder_stats(
|
||||||
socket: &std::path::Path,
|
socket: &std::path::Path,
|
||||||
window_secs: u64,
|
window_secs: u64,
|
||||||
) -> Option<hive_sh4re::ReminderStats> {
|
) -> Option<hive_sh4re::ReminderStats> {
|
||||||
match client::request::<_, hive_sh4re::Response>(
|
match tokio::time::timeout(
|
||||||
socket,
|
SOCKET_FETCH_TIMEOUT,
|
||||||
&hive_sh4re::Request::ReminderRollup {
|
client::request::<_, hive_sh4re::Response>(
|
||||||
since_secs: window_secs,
|
socket,
|
||||||
agent: None,
|
&hive_sh4re::Request::ReminderRollup {
|
||||||
},
|
since_secs: window_secs,
|
||||||
|
agent: None,
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(hive_sh4re::Response::ReminderRollup(stats)) => Some(stats),
|
Ok(Ok(hive_sh4re::Response::ReminderRollup(stats))) => Some(stats),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -859,16 +879,20 @@ async fn post_send(State(state): State<AppState>, Form(form): Form<SendForm>) ->
|
||||||
if body.is_empty() {
|
if body.is_empty() {
|
||||||
return error_response("send: `body` required");
|
return error_response("send: `body` required");
|
||||||
}
|
}
|
||||||
let result = match client::request::<_, hive_sh4re::Response>(
|
let result = match tokio::time::timeout(
|
||||||
&state.socket,
|
SOCKET_FETCH_TIMEOUT,
|
||||||
&hive_sh4re::Request::OperatorMsg { body },
|
client::request::<_, hive_sh4re::Response>(
|
||||||
|
&state.socket,
|
||||||
|
&hive_sh4re::Request::OperatorMsg { body },
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(hive_sh4re::Response::Ok) => Ok(()),
|
Ok(Ok(hive_sh4re::Response::Ok)) => Ok(()),
|
||||||
Ok(hive_sh4re::Response::Err { message }) => Err(message),
|
Ok(Ok(hive_sh4re::Response::Err { message })) => Err(message),
|
||||||
Ok(other) => Err(format!("unexpected response: {other:?}")),
|
Ok(Ok(other)) => Err(format!("unexpected response: {other:?}")),
|
||||||
Err(e) => Err(format!("transport: {e:#}")),
|
Ok(Err(e)) => Err(format!("transport: {e:#}")),
|
||||||
|
Err(_) => Err("timed out — hive-c0re busy, retry".to_owned()),
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
// 200 instead of 303 → the client doesn't refetch /api/state.
|
// 200 instead of 303 → the client doesn't refetch /api/state.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue