fix(supervisor): compute uptime at read time, not publish time

set_state computed uptime_secs from started_at.elapsed() and pushed the
result into the watch channel. do_start sets started_at immediately before
calling set_state(Running), so the published value was always ~0, and the
list/status handlers copied that frozen snapshot straight out. A server
that stayed healthy reported 0s forever, because the value was only
recomputed on the next state transition.

Status now carries started_at (an Instant, in-process only) and the
handlers derive uptime_secs when they build ServerSummary. The wire type
is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
This commit is contained in:
2026-08-07 13:11:01 +02:00
co-authored by Claude Opus 5
parent d75d57ff0b
commit 0c8ae3b617
4 changed files with 46 additions and 8 deletions
+3 -5
View File
@@ -48,7 +48,7 @@ pub struct Status {
pub state: ServerState, pub state: ServerState,
pub pid: Option<u32>, pub pid: Option<u32>,
pub port: u16, pub port: u16,
pub uptime_secs: Option<u64>, pub started_at: Option<Instant>,
pub restart_count: u32, pub restart_count: u32,
pub last_exit: Option<i32>, pub last_exit: Option<i32>,
} }
@@ -109,13 +109,11 @@ impl<S: Spawner> SupervisorTask<S> {
} }
fn set_state(&mut self, s: ServerState) { fn set_state(&mut self, s: ServerState) {
let uptime_secs = self.started_at.map(|t| t.elapsed().as_secs());
let _ = self.status_tx.send(Status { let _ = self.status_tx.send(Status {
state: s, state: s,
pid: self.current_pid, pid: self.current_pid,
port: self.cfg.port, port: self.cfg.port,
uptime_secs, started_at: self.started_at,
restart_count: self.restart_count, restart_count: self.restart_count,
last_exit: self.last_exit, last_exit: self.last_exit,
}); });
@@ -407,7 +405,7 @@ mod tests {
state: ServerState::Stopped, state: ServerState::Stopped,
pid: None, pid: None,
port: cfg.port, port: cfg.port,
uptime_secs: None, started_at: None,
restart_count: 0, restart_count: 0,
last_exit: None, last_exit: None,
} }
+2 -2
View File
@@ -170,7 +170,7 @@ async fn list(reg: &Registry) -> Result<Vec<ServerSummary>, ApiError> {
state: s.state, state: s.state,
pid: s.pid, pid: s.pid,
port: s.port, port: s.port,
uptime_secs: s.uptime_secs, uptime_secs: s.started_at.map(|t| t.elapsed().as_secs()),
restart_count: s.restart_count, restart_count: s.restart_count,
last_exit: s.last_exit, last_exit: s.last_exit,
}); });
@@ -195,7 +195,7 @@ async fn status(reg: &Registry, name: &str) -> Result<StatusDetail, ApiError> {
state: s.state, state: s.state,
pid: s.pid, pid: s.pid,
port: s.port, port: s.port,
uptime_secs: s.uptime_secs, uptime_secs: s.started_at.map(|t| t.elapsed().as_secs()),
restart_count: s.restart_count, restart_count: s.restart_count,
last_exit: s.last_exit, last_exit: s.last_exit,
}, },
+1 -1
View File
@@ -43,7 +43,7 @@ pub fn spawn_supervisor(paths: &Paths, cfg: ServerConfig) -> Result<SupervisorHa
state: ServerState::Stopped, state: ServerState::Stopped,
pid: None, pid: None,
port: cfg.port, port: cfg.port,
uptime_secs: None, started_at: None,
restart_count: 0, restart_count: 0,
last_exit: None, last_exit: None,
}; };
+40
View File
@@ -0,0 +1,40 @@
mod common;
use common::*;
#[tokio::test]
async fn uptime_grows_while_a_server_stays_running() {
let xy = xy_bin();
let sleeper = sleep_server_bin();
let mut h = Harness::new();
h.write_server("alpha", sleeper.to_str().unwrap(), 19_101, "always");
h.start_daemon(&xy).await;
for _ in 0..40 {
let (_c, out, _e) = h.run_cli(&xy, &["list"]).await;
if out.contains("alpha") && out.contains("running") {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
tokio::time::sleep(std::time::Duration::from_millis(1_200)).await;
let (code, out, _e) = h.run_cli(&xy, &["list"]).await;
assert_eq!(code, 0);
let row = out
.lines()
.find(|l| l.starts_with("alpha"))
.unwrap_or_else(|| panic!("no alpha row in:\n{out}"));
let uptime = row
.split_whitespace()
.nth(4)
.unwrap_or_else(|| panic!("no uptime column in row: {row:?}"));
assert_ne!(
uptime, "0s",
"a server running for over a second must not report 0s uptime; row: {row:?}"
);
}