diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index ead302ee..295532d4 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -200,6 +200,41 @@ pub async fn ensure_manager(coord: &Arc) -> Result<()> { Ok(()) } +/// Sort `names` in-place so parents precede their children in the topology. +/// Uses BFS from root agents (depth 0). Agents absent from `topo` sort last, +/// alphabetically within their tier. Stable within each depth tier. +fn topology_sort(names: &mut Vec, topo: &std::collections::BTreeMap>) { + use std::collections::{HashMap, VecDeque}; + // Build depth map using owned clones so the borrow on `names` is released + // before the sort_by mutable borrow. + let name_set: Vec = names.clone(); + let mut depth: HashMap = HashMap::new(); + let mut queue: VecDeque = VecDeque::new(); + // Seed roots: entries with no parent, or names not present in topo at all. + for name in &name_set { + if topo.get(name).map_or(true, |p| p.is_none()) { + depth.insert(name.clone(), 0); + queue.push_back(name.clone()); + } + } + // BFS to assign depths to children. + while let Some(parent) = queue.pop_front() { + let d = depth[&parent] + 1; + for name in &name_set { + let is_child = topo.get(name).and_then(|p| p.as_deref()) == Some(parent.as_str()); + if is_child && !depth.contains_key(name) { + depth.insert(name.clone(), d); + queue.push_back(name.clone()); + } + } + } + names.sort_by(|a, b| { + let da = depth.get(a).copied().unwrap_or(usize::MAX); + let db = depth.get(b).copied().unwrap_or(usize::MAX); + da.cmp(&db).then(a.cmp(b)) + }); +} + /// Rebuild every container on startup. Enqueues a `StartupSweep` parent /// entry (agent = `"hyperhive"`) followed by per-agent `Rebuild` children /// linked via `parent_id`. The dashboard renders them nested so the operator @@ -231,13 +266,23 @@ pub async fn run(coord: Arc) -> Result<()> { "auto-update: queueing all on startup" ); - for container in &containers { - let logical = if container == MANAGER_NAME { - Some(MANAGER_NAME.to_owned()) - } else { - container.strip_prefix(AGENT_PREFIX).map(str::to_owned) - }; - let Some(name) = logical else { continue }; + // Resolve container names to logical agent names, then sort by + // topology depth so parents are always rebuilt before their + // children. Root agents (depth 0) go first; agents absent from + // the topology file sort last (stable, alphabetical within tier). + let mut logical_names: Vec = containers + .iter() + .filter_map(|c| { + if c == MANAGER_NAME { + Some(MANAGER_NAME.to_owned()) + } else { + c.strip_prefix(AGENT_PREFIX).map(str::to_owned) + } + }) + .collect(); + let topo = crate::topology::read(); + topology_sort(&mut logical_names, &topo); + for name in logical_names { coord.rebuild_queue.enqueue( crate::rebuild_queue::QueueKind::Rebuild, name,