From a40b61aeff6fe282c03eed4539a57254de0a10a0 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Sat, 4 Jul 2026 00:25:22 +0100 Subject: [PATCH] fix: remove stuck node upgrade-scheduled status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daemon flipped a running node to `UpgradeScheduled` from a 60s poll the moment its on-disk binary version drifted, but only returned it to `Running` on process exit + respawn. When the exit never followed (false-positive drift, an adopted node still alive, or a fast/Windows upgrade the poll missed), the node stayed `UpgradeScheduled` indefinitely. Remove the status entirely rather than fix it: the window it represented was tiny and not useful. Upgrades are now detected purely at process exit by on-disk binary version drift for exit codes 0 (Unix) and 100 (Windows RESTART_EXIT_CODE) — which also fixes the pre-existing misclassification of Windows and fast upgrades that the exit-code-0-only path missed. Removes the UpgradeScheduled status variant, the spawn_upgrade_monitor poll, mark_upgrade_scheduled, the pending_version field/plumbing, and the UpgradeScheduled SSE event. Version refresh (respawn_upgraded_node) and the NodeUpgraded completion event are retained. Backwards compatible: the status is never persisted, and a serde alias on Running maps any `upgrade_scheduled` value emitted by an older daemon during a rolling upgrade back to Running. Co-Authored-By: Claude Opus 4.8 (1M context) --- ant-cli/src/commands/node/status.rs | 9 +- ant-core/src/node/daemon/server.rs | 30 +-- ant-core/src/node/daemon/supervisor.rs | 289 ++++--------------------- ant-core/src/node/events.rs | 20 -- ant-core/src/node/mod.rs | 1 - ant-core/src/node/types.rs | 53 +---- 6 files changed, 64 insertions(+), 338 deletions(-) diff --git a/ant-cli/src/commands/node/status.rs b/ant-cli/src/commands/node/status.rs index 9617888..d521672 100644 --- a/ant-cli/src/commands/node/status.rs +++ b/ant-cli/src/commands/node/status.rs @@ -66,20 +66,13 @@ impl StatusArgs { NodeStatus::Starting => format!("{} {}", "●".yellow(), "Starting".yellow()), NodeStatus::Stopping => format!("{} {}", "●".yellow(), "Stopping".yellow()), NodeStatus::Errored => format!("{} {}", "●".red(), "Errored".red()), - NodeStatus::UpgradeScheduled => { - format!("{} {}", "●".cyan(), "Upgrade scheduled".cyan()) - } NodeStatus::Evicted => format!("{} {}", "●".magenta(), "Evicted".magenta()), }; - let version_display = match &node.pending_version { - Some(pending) => format!("{} → {}", node.version, pending), - None => node.version.clone(), - }; println!( " {:<4} {:<14} {:<18} {}", node.node_id.to_string().bold(), node.name, - version_display.dimmed(), + node.version.dimmed(), status_display ); // Supplementary text explaining an eviction, plus how to clear it. diff --git a/ant-core/src/node/daemon/server.rs b/ant-core/src/node/daemon/server.rs index 19f629f..5747a4c 100644 --- a/ant-core/src/node/daemon/server.rs +++ b/ant-core/src/node/daemon/server.rs @@ -17,8 +17,8 @@ use crate::error::Result; use crate::node::binary::NoopProgress; use crate::node::daemon::health::{DiskThresholds, FleetHealth}; use crate::node::daemon::supervisor::{ - spawn_eviction_monitor, spawn_liveness_monitor, spawn_upgrade_monitor, Supervisor, - EVICTION_POLL_INTERVAL, LIVENESS_POLL_INTERVAL, UPGRADE_POLL_INTERVAL, + spawn_eviction_monitor, spawn_liveness_monitor, Supervisor, EVICTION_POLL_INTERVAL, + LIVENESS_POLL_INTERVAL, }; use crate::node::events::NodeEvent; use crate::node::registry::NodeRegistry; @@ -98,16 +98,6 @@ pub async fn start( health: health.clone(), }); - // Background task: probe each Running node's on-disk binary for version drift caused by - // ant-node's auto-upgrade, and flip them to UpgradeScheduled so the supervisor knows the - // next exit is expected. - spawn_upgrade_monitor( - registry.clone(), - supervisor.clone(), - UPGRADE_POLL_INTERVAL, - shutdown.clone(), - ); - // Background task: monitor free disk space at node data directories. Refreshes the fleet health // snapshot every tick and auto-evicts a node (smallest data dir) on any partition that has // fallen to the eviction threshold while ≥2 nodes remain. The threshold is a fixed internal @@ -258,19 +248,16 @@ async fn get_nodes_status(State(state): State>) -> Json { - total_running += 1 - } + NodeStatus::Running | NodeStatus::Starting => total_running += 1, _ => total_stopped += 1, } - let (pid, uptime_secs, pending_version) = if config.eviction.is_some() { - (None, None, None) + let (pid, uptime_secs) = if config.eviction.is_some() { + (None, None) } else { ( supervisor.node_pid(config.id), supervisor.node_uptime_secs(config.id), - supervisor.node_pending_version(config.id), ) }; @@ -281,7 +268,6 @@ async fn get_nodes_status(State(state): State>) -> Json, restart_count: u32, first_crash_at: Option, - /// When `status == UpgradeScheduled`, the target version the on-disk binary now reports. - pending_version: Option, } impl Supervisor { @@ -319,7 +320,6 @@ impl Supervisor { started_at: None, restart_count: 0, first_crash_at: None, - pending_version: None, }, ); return Err(Error::ProcessSpawn(format!( @@ -342,7 +342,6 @@ impl Supervisor { started_at: Some(Instant::now()), restart_count: 0, first_crash_at: None, - pending_version: None, }, ); // This daemon now owns the process and spawns a `monitor_node` for it below, so it is @@ -469,34 +468,6 @@ impl Supervisor { .and_then(|s| s.started_at.map(|t| t.elapsed().as_secs())) } - /// The target version when the node is in `UpgradeScheduled` state, otherwise `None`. - pub fn node_pending_version(&self, node_id: u32) -> Option { - self.node_states - .get(&node_id) - .and_then(|s| s.pending_version.clone()) - } - - /// Transition a Running node into `UpgradeScheduled` with the target version. - /// - /// Only affects nodes currently in `Running`: any other state is left alone (a stopped - /// node legitimately has an out-of-date binary; a node already in UpgradeScheduled has - /// already been marked). Returns `true` if the transition happened. - fn mark_upgrade_scheduled(&mut self, node_id: u32, pending_version: String) -> bool { - let Some(state) = self.node_states.get_mut(&node_id) else { - return false; - }; - if state.status != NodeStatus::Running { - return false; - } - state.status = NodeStatus::UpgradeScheduled; - state.pending_version = Some(pending_version.clone()); - let _ = self.event_tx.send(NodeEvent::UpgradeScheduled { - node_id, - pending_version, - }); - true - } - /// Check whether a node is running. pub fn is_running(&self, node_id: u32) -> bool { self.node_states @@ -511,10 +482,7 @@ impl Supervisor { let mut errored = 0u32; for state in self.node_states.values() { match state.status { - // UpgradeScheduled means the process is still running; count it with running. - NodeStatus::Running | NodeStatus::Starting | NodeStatus::UpgradeScheduled => { - running += 1 - } + NodeStatus::Running | NodeStatus::Starting => running += 1, // An evicted node is not running; count it alongside stopped for these totals. NodeStatus::Stopped | NodeStatus::Stopping | NodeStatus::Evicted => stopped += 1, NodeStatus::Errored => errored += 1, @@ -594,7 +562,6 @@ impl Supervisor { started_at: Some(process_started_at(&sys, pid).unwrap_or_else(Instant::now)), restart_count: 0, first_crash_at: None, - pending_version: None, }, ); // No owning `monitor_node` exists for an adopted process (its `Child` died with the @@ -654,77 +621,6 @@ impl Supervisor { } } -/// Periodically probe each Running node's on-disk binary for a version change. -/// -/// When a node's binary-on-disk reports a different version than was recorded in the registry -/// at `ant node add` time, ant-node has replaced the binary in place as part of its auto-upgrade -/// flow and will restart the process shortly. We flip the node to `UpgradeScheduled` with the -/// target version, which lets `ant node status` render the in-between state and lets -/// `monitor_node` reclassify the upcoming clean exit as an expected restart rather than a crash. -/// -/// The task exits when `shutdown` is cancelled. -pub fn spawn_upgrade_monitor( - registry: Arc>, - supervisor: Arc>, - interval: Duration, - shutdown: CancellationToken, -) { - tokio::spawn(async move { - let mut ticker = tokio::time::interval(interval); - // After a Windows sleep/hibernate the default `Burst` catch-up would fire one - // tick per missed interval back-to-back, producing a flood of `extract_version` - // subprocess spawns. `Skip` resumes on the next aligned tick instead. - ticker.set_missed_tick_behavior(MissedTickBehavior::Skip); - // Skip the immediate first tick — we don't want to probe while nodes are still in the - // Starting -> Running transition. - ticker.tick().await; - - loop { - tokio::select! { - _ = shutdown.cancelled() => return, - _ = ticker.tick() => {}, - } - - // Collect a snapshot of (node_id, binary_path, recorded_version, current_pending) - // to release the locks before running --version subprocesses (which take time). - let candidates: Vec<(u32, std::path::PathBuf, String, Option)> = { - let reg = registry.read().await; - let sup = supervisor.read().await; - reg.list() - .into_iter() - .filter_map(|config| match sup.node_status(config.id) { - Ok(NodeStatus::Running) => Some(( - config.id, - config.binary_path.clone(), - config.version.clone(), - sup.node_pending_version(config.id), - )), - _ => None, - }) - .collect() - }; - - for (node_id, binary_path, recorded_version, current_pending) in candidates { - let observed = match extract_version(&binary_path).await { - Ok(v) => v, - // Transient failures (e.g. binary mid-replacement) — skip this round. - Err(_) => continue, - }; - if observed == recorded_version { - continue; - } - if current_pending.as_deref() == Some(observed.as_str()) { - continue; - } - supervisor - .write() - .await - .mark_upgrade_scheduled(node_id, observed); - } - } - }); -} - /// Background task: monitor free disk space at each node's data directory and, when a partition /// falls to its eviction threshold, automatically evict a node to reclaim space. /// @@ -1056,6 +952,15 @@ pub fn build_node_args(config: &NodeConfig) -> Vec { args } +/// Whether `exit_code` is one ant-node uses to hand its restart to this daemon after replacing its +/// own binary during an auto-upgrade: `0` on Unix, [`RESTART_EXIT_CODE`] on Windows (both under +/// `--stop-on-upgrade`, which the daemon always sets). A matching code is necessary but not +/// sufficient — the caller additionally confirms the on-disk binary version drifted before treating +/// the exit as an upgrade rather than a crash. +fn is_upgrade_restart_exit_code(exit_code: Option) -> bool { + matches!(exit_code, Some(0) | Some(RESTART_EXIT_CODE)) +} + /// Spawn a node process from a NodeConfig. /// /// Writes `/node.pid` on successful spawn so that a future daemon instance @@ -1104,60 +1009,34 @@ async fn monitor_node_inner( // Wait for the process to exit let exit_status = child.wait().await; - // Check whether this is a scheduled upgrade restart or an intentional stop. + // Intentional stops must not respawn. Stopped/Stopping are user-initiated; Evicted means + // the daemon deleted the data dir to reclaim space. let status_at_exit = { let sup = supervisor.read().await; sup.node_status(node_id).ok() }; - - match status_at_exit { - // Stopped/Stopping are intentional; Evicted means the daemon deleted the data dir to - // reclaim space. In all three cases the node must not be respawned. - Some(NodeStatus::Stopped) | Some(NodeStatus::Stopping) | Some(NodeStatus::Evicted) => { - return - } - Some(NodeStatus::UpgradeScheduled) => { - // ant-node cleanly exited after replacing its binary in place. Respawn - // directly (no backoff, no crash counter) and refresh the recorded version. - match respawn_upgraded_node(config, &supervisor, ®istry, &event_tx).await { - Ok(new_child) => { - child = new_child; - continue; - } - Err(e) => { - let _ = event_tx.send(NodeEvent::NodeErrored { - node_id, - message: format!("Failed to respawn after upgrade: {e}"), - }); - let mut sup = supervisor.write().await; - sup.update_state(node_id, NodeStatus::Errored, None); - return; - } - } - } - _ => {} + if matches!( + status_at_exit, + Some(NodeStatus::Stopped) | Some(NodeStatus::Stopping) | Some(NodeStatus::Evicted) + ) { + return; } let exit_code = exit_status.ok().and_then(|s| s.code()); - // A process-reported exit that wasn't user-initiated (Stopping was filtered above) is - // either an auto-upgrade (exit 0 after ant-node replaced its binary) or a crash. In - // neither case should the node be parked in `Stopped` — that state is reserved for - // intentional user stops. + // A process-reported exit that wasn't user-initiated (filtered above) is either an + // auto-upgrade or a crash. In neither case should the node be parked in `Stopped` — that + // state is reserved for intentional user stops. // - // Distinguish upgrade from crash by checking whether the on-disk binary's version - // drifted from the registry. Between replacing its binary and actually exiting, - // ant-node can hold the process open for anywhere from seconds to minutes, depending - // on in-flight work and its own config. The periodic version poll will usually have - // flipped the node to `UpgradeScheduled` well before the exit, but when the window is - // short we cannot rely on that — hence this synchronous re-check here. - if exit_code == Some(0) { + // ant-node runs with `--stop-on-upgrade`: after replacing its own binary in place it exits + // cleanly and relies on this daemon to restart it (`0` on Unix, `RESTART_EXIT_CODE` on + // Windows). Distinguish an upgrade from a crash by whether the on-disk binary's version + // drifted from the registry — the reliable signal, independent of platform exit code. On an + // upgrade we respawn directly (no backoff, no crash counter) and refresh the recorded + // version via `respawn_upgraded_node`. + if is_upgrade_restart_exit_code(exit_code) { if let Ok(disk_version) = extract_version(&config.binary_path).await { if disk_version != config.version { - { - let mut sup = supervisor.write().await; - sup.mark_upgrade_scheduled(node_id, disk_version.clone()); - } match respawn_upgraded_node(config, &supervisor, ®istry, &event_tx).await { Ok(new_child) => { child = new_child; @@ -1175,9 +1054,9 @@ async fn monitor_node_inner( } } } - // Exit 0 but the binary didn't change — fall through to the crash / restart path. - // We report the crash with the exit code preserved; the crash counter guards - // against infinite restart loops if the process keeps exiting immediately. + // Clean exit but the binary didn't change — fall through to the crash / restart path. + // We report the crash with the exit code preserved; the crash counter guards against + // infinite restart loops if the process keeps exiting immediately. } // Crash (or clean exit that wasn't an upgrade) @@ -1241,10 +1120,10 @@ async fn monitor_node_inner( } } -/// Respawn a node whose `UpgradeScheduled` status tells us the exit was expected. +/// Respawn a node that exited to apply an in-place auto-upgrade of its own binary. /// /// On success: persists the new version to the registry, updates the in-memory config clone, -/// clears pending_version, sets status back to Running, and fires `NodeUpgraded`. +/// sets status back to Running, and fires `NodeUpgraded`. async fn respawn_upgraded_node( config: &mut NodeConfig, supervisor: &Arc>, @@ -1278,7 +1157,6 @@ async fn respawn_upgraded_node( state.status = NodeStatus::Running; state.pid = Some(pid); state.started_at = Some(Instant::now()); - state.pending_version = None; state.restart_count = 0; state.first_crash_at = None; } @@ -1811,7 +1689,6 @@ mod tests { started_at: Some(Instant::now()), restart_count: 0, first_crash_at: None, - pending_version: None, }, ); @@ -1855,7 +1732,6 @@ mod tests { started_at: Some(Instant::now()), restart_count: 0, first_crash_at: None, - pending_version: None, }, ); sup.node_states.insert( @@ -1866,7 +1742,6 @@ mod tests { started_at: None, restart_count: 0, first_crash_at: None, - pending_version: None, }, ); sup.node_states.insert( @@ -1877,7 +1752,6 @@ mod tests { started_at: None, restart_count: 5, first_crash_at: None, - pending_version: None, }, ); @@ -1888,83 +1762,15 @@ mod tests { } #[test] - fn mark_upgrade_scheduled_only_affects_running_nodes() { - let (tx, mut rx) = broadcast::channel(16); - let mut sup = Supervisor::new(tx); - - sup.node_states.insert( - 1, - NodeRuntime { - status: NodeStatus::Running, - pid: Some(111), - started_at: Some(Instant::now()), - restart_count: 0, - first_crash_at: None, - pending_version: None, - }, - ); - sup.node_states.insert( - 2, - NodeRuntime { - status: NodeStatus::Stopped, - pid: None, - started_at: None, - restart_count: 0, - first_crash_at: None, - pending_version: None, - }, - ); - - // Running node: transitions to UpgradeScheduled with pending_version set and event fires. - let affected = sup.mark_upgrade_scheduled(1, "0.10.11-rc.1".to_string()); - assert!(affected); - assert_eq!(sup.node_status(1).unwrap(), NodeStatus::UpgradeScheduled); - assert_eq!(sup.node_pending_version(1).as_deref(), Some("0.10.11-rc.1")); - match rx.try_recv() { - Ok(NodeEvent::UpgradeScheduled { - node_id, - pending_version, - }) => { - assert_eq!(node_id, 1); - assert_eq!(pending_version, "0.10.11-rc.1"); - } - other => panic!("expected UpgradeScheduled event, got {other:?}"), - } - - // Stopped node: untouched, no event fired. - let affected = sup.mark_upgrade_scheduled(2, "0.10.11-rc.1".to_string()); - assert!(!affected); - assert_eq!(sup.node_status(2).unwrap(), NodeStatus::Stopped); - assert!(sup.node_pending_version(2).is_none()); - - // Already-UpgradeScheduled node: calling again is a no-op. - let affected = sup.mark_upgrade_scheduled(1, "0.10.12".to_string()); - assert!(!affected); - // Pending version is the original one set. - assert_eq!(sup.node_pending_version(1).as_deref(), Some("0.10.11-rc.1")); - } - - #[test] - fn node_counts_counts_upgrade_scheduled_as_running() { - let (tx, _rx) = broadcast::channel(16); - let mut sup = Supervisor::new(tx); - - sup.node_states.insert( - 1, - NodeRuntime { - status: NodeStatus::UpgradeScheduled, - pid: Some(111), - started_at: Some(Instant::now()), - restart_count: 0, - first_crash_at: None, - pending_version: Some("0.10.11-rc.1".to_string()), - }, - ); - - let (running, stopped, errored) = sup.node_counts(); - assert_eq!(running, 1); - assert_eq!(stopped, 0); - assert_eq!(errored, 0); + fn upgrade_restart_exit_code_covers_unix_and_windows() { + // Unix upgrade exit and the Windows RESTART_EXIT_CODE both count as candidate upgrade + // restarts; anything else (crash codes, signals with no code) does not. The version-drift + // check in `monitor_node_inner` is what actually confirms an upgrade — this only gates it. + assert!(is_upgrade_restart_exit_code(Some(0))); + assert!(is_upgrade_restart_exit_code(Some(RESTART_EXIT_CODE))); + assert!(!is_upgrade_restart_exit_code(Some(1))); + assert!(!is_upgrade_restart_exit_code(Some(101))); + assert!(!is_upgrade_restart_exit_code(None)); } #[tokio::test] @@ -1989,7 +1795,6 @@ mod tests { started_at: None, restart_count: 0, first_crash_at: None, - pending_version: None, }, ); @@ -2011,7 +1816,6 @@ mod tests { started_at: Some(Instant::now()), restart_count: 0, first_crash_at: None, - pending_version: None, }, ); // Node 2: already stopped @@ -2023,7 +1827,6 @@ mod tests { started_at: None, restart_count: 0, first_crash_at: None, - pending_version: None, }, ); diff --git a/ant-core/src/node/events.rs b/ant-core/src/node/events.rs index 3c91523..f13a84a 100644 --- a/ant-core/src/node/events.rs +++ b/ant-core/src/node/events.rs @@ -44,12 +44,6 @@ pub enum NodeEvent { version: String, path: PathBuf, }, - /// Emitted when the supervisor detects that a node's on-disk binary has been - /// replaced by its auto-upgrade, ahead of the node process restarting. - UpgradeScheduled { - node_id: u32, - pending_version: String, - }, /// Emitted after the supervisor has respawned a node against its replaced binary and /// observed the new version. NodeUpgraded { @@ -88,7 +82,6 @@ impl NodeEvent { NodeEvent::DownloadStarted { .. } => "download_started", NodeEvent::DownloadProgress { .. } => "download_progress", NodeEvent::DownloadComplete { .. } => "download_complete", - NodeEvent::UpgradeScheduled { .. } => "upgrade_scheduled", NodeEvent::NodeUpgraded { .. } => "node_upgraded", NodeEvent::NodeEvicted { .. } => "node_evicted", NodeEvent::FleetHealthChanged { .. } => "fleet_health_changed", @@ -141,19 +134,6 @@ mod tests { assert_eq!(deserialized.event_type(), "download_progress"); } - #[test] - fn upgrade_scheduled_event_serializes() { - let event = NodeEvent::UpgradeScheduled { - node_id: 2, - pending_version: "0.10.11-rc.1".to_string(), - }; - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"type\":\"upgrade_scheduled\"")); - assert!(json.contains("\"node_id\":2")); - assert!(json.contains("\"pending_version\":\"0.10.11-rc.1\"")); - assert_eq!(event.event_type(), "upgrade_scheduled"); - } - #[test] fn node_upgraded_event_serializes() { let event = NodeEvent::NodeUpgraded { diff --git a/ant-core/src/node/mod.rs b/ant-core/src/node/mod.rs index b311301..86d6038 100644 --- a/ant-core/src/node/mod.rs +++ b/ant-core/src/node/mod.rs @@ -214,7 +214,6 @@ pub fn node_status_offline(registry_path: &Path) -> Result { status, pid: None, uptime_secs: None, - pending_version: None, eviction: config.eviction.clone(), } }) diff --git a/ant-core/src/node/types.rs b/ant-core/src/node/types.rs index 56fadb2..70481e5 100644 --- a/ant-core/src/node/types.rs +++ b/ant-core/src/node/types.rs @@ -67,13 +67,13 @@ pub struct DaemonInfo { pub enum NodeStatus { Stopped, Starting, + /// The `upgrade_scheduled` alias accepts the retired status emitted by pre-removal daemons: an + /// upgrade-scheduled node is functionally running, so a newer client reading an older daemon's + /// status maps the old value here rather than failing to deserialize. + #[serde(alias = "upgrade_scheduled")] Running, Stopping, Errored, - /// The node's on-disk binary has been replaced by an auto-upgrade, but the process has not - /// yet restarted. The supervisor is waiting for the current process to exit and will then - /// respawn it against the new binary. - UpgradeScheduled, /// The daemon automatically stopped this node and deleted its data directory to reclaim disk /// space for the remaining nodes. This is a terminal state derived from a persisted /// [`EvictionRecord`] on the node's config — it survives daemon restarts and is cleared only @@ -136,10 +136,6 @@ pub struct NodeInfo { pub status: NodeStatus, pub pid: Option, pub uptime_secs: Option, - /// Set only when `status == UpgradeScheduled`: the new version that the replaced on-disk - /// binary reports. Omitted otherwise. - #[serde(skip_serializing_if = "Option::is_none")] - pub pending_version: Option, } /// Result of a daemon start operation. @@ -448,10 +444,6 @@ pub struct NodeStatusSummary { /// Seconds since the node process started (only set when running). #[serde(skip_serializing_if = "Option::is_none")] pub uptime_secs: Option, - /// Set only when `status == UpgradeScheduled`: the new version that the replaced on-disk - /// binary reports. Omitted otherwise. - #[serde(skip_serializing_if = "Option::is_none")] - pub pending_version: Option, /// Set only when `status == Evicted`: details of why/when the node was evicted, so the CLI and /// GUI can show supplementary text. Omitted otherwise. #[serde(skip_serializing_if = "Option::is_none")] @@ -505,11 +497,12 @@ mod tests { } #[test] - fn node_status_upgrade_scheduled_serializes() { - let json = serde_json::to_string(&NodeStatus::UpgradeScheduled).unwrap(); - assert_eq!(json, "\"upgrade_scheduled\""); - let parsed: NodeStatus = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed, NodeStatus::UpgradeScheduled); + fn node_status_upgrade_scheduled_deserializes_as_running() { + // Backwards compatibility: the retired `upgrade_scheduled` status emitted by a pre-removal + // daemon must still deserialize (as the functionally-equivalent Running) so a newer client + // talking to an older daemon during a rolling upgrade doesn't error. + let parsed: NodeStatus = serde_json::from_str("\"upgrade_scheduled\"").unwrap(); + assert_eq!(parsed, NodeStatus::Running); } #[test] @@ -551,7 +544,6 @@ mod tests { status: NodeStatus::Evicted, pid: None, uptime_secs: None, - pending_version: None, eviction: Some(EvictionRecord { reason: "Low disk: 480 MiB free, evicting smallest node".to_string(), evicted_at: 1_700_000_000, @@ -565,25 +557,6 @@ mod tests { assert_eq!(parsed.eviction.unwrap().evicted_at, 1_700_000_000); } - #[test] - fn node_status_summary_with_pending_version() { - let summary = NodeStatusSummary { - node_id: 7, - name: "antnode-7".to_string(), - version: "0.10.1".to_string(), - status: NodeStatus::UpgradeScheduled, - pid: Some(4242), - uptime_secs: Some(3600), - pending_version: Some("0.10.11-rc.1".to_string()), - eviction: None, - }; - let json = serde_json::to_string(&summary).unwrap(); - assert!(json.contains("\"status\":\"upgrade_scheduled\"")); - assert!(json.contains("\"pending_version\":\"0.10.11-rc.1\"")); - let roundtrip: NodeStatusSummary = serde_json::from_str(&json).unwrap(); - assert_eq!(roundtrip.pending_version.as_deref(), Some("0.10.11-rc.1")); - } - #[test] fn port_range_single_len() { let pr = PortRange::Single(8080); @@ -665,7 +638,6 @@ mod tests { status: NodeStatus::Running, pid: Some(1234), uptime_secs: Some(60), - pending_version: None, eviction: None, }, NodeStatusSummary { @@ -675,7 +647,6 @@ mod tests { status: NodeStatus::Stopped, pid: None, uptime_secs: None, - pending_version: None, eviction: None, }, ], @@ -701,7 +672,6 @@ mod tests { status: NodeStatus::Running, pid: Some(5678), uptime_secs: Some(120), - pending_version: None, eviction: None, }; let json = serde_json::to_string(&summary).unwrap(); @@ -711,7 +681,6 @@ mod tests { assert!(json.contains("\"status\":\"running\"")); assert!(json.contains("\"pid\":5678")); assert!(json.contains("\"uptime_secs\":120")); - assert!(!json.contains("pending_version")); // None fields should be omitted let stopped = NodeStatusSummary { @@ -721,13 +690,11 @@ mod tests { status: NodeStatus::Stopped, pid: None, uptime_secs: None, - pending_version: None, eviction: None, }; let json_stopped = serde_json::to_string(&stopped).unwrap(); assert!(!json_stopped.contains("pid")); assert!(!json_stopped.contains("uptime_secs")); - assert!(!json_stopped.contains("pending_version")); } #[test]