fix(#1218): bulk topology move uses one git commit via new set-parent-bulk endpoint

This commit is contained in:
damocles 2026-06-03 22:57:40 +02:00 committed by mara
commit 7ec0a36d7a
4 changed files with 179 additions and 4 deletions

View file

@ -397,6 +397,69 @@ pub async fn commit_topology(
Ok(())
}
/// Batch variant of [`commit_topology`]: applies every `(child, new_parent)`
/// move under a single `META_LOCK` acquisition and creates **one** git commit
/// for all of them. Moves are applied in the order given; the first
/// validation error short-circuits the whole batch (no partial writes because
/// `topology::set_parent` rewrites the in-memory map atomically and we only
/// flush to disk after all moves have passed validation).
///
/// Returns a `Vec` of `(child, old_parent)` pairs for every move that
/// actually changed the topology (idempotent same-parent moves are skipped),
/// so the caller can send targeted notifications.
pub async fn bulk_commit_topology(
moves: &[(&str, Option<&str>)],
) -> std::result::Result<Vec<(String, Option<String>)>, String> {
if moves.is_empty() {
return Ok(vec![]);
}
let _guard = META_LOCK.lock().await;
// Snapshot parents before any writes so we can compute the diff.
let topo_before = crate::topology::read();
// Validate + apply all moves in sequence; first error aborts.
for (child, new_parent) in moves {
crate::topology::set_parent(child, *new_parent)?;
}
// Commit the whole batch as one git operation.
let dir = meta_dir();
let commit_msg = if moves.len() == 1 {
let (child, new_parent) = moves[0];
format!("topology: {}{}", child, new_parent.unwrap_or("<root>"))
} else {
let names: Vec<&str> = moves.iter().map(|(c, _)| *c).collect();
let dest = moves[0].1.unwrap_or("<root>");
format!(
"topology: move {} agents → {} ({})",
moves.len(),
dest,
names.join(", ")
)
};
let stage = async {
git(&dir, &["add", "topology.json"]).await?;
if has_staged_changes(&dir).await? {
git_commit(&dir, &commit_msg).await?;
}
Ok::<_, anyhow::Error>(())
};
if let Err(e) = stage.await {
tracing::warn!(error = ?e, "bulk_commit_topology: topology written but git commit failed (sync_agents will recover)");
}
// Return (child, old_parent) for each move that changed state.
let changed = moves
.iter()
.filter_map(|(child, new_parent)| {
let old = topo_before.get(*child).cloned().flatten();
if old.as_deref() != *new_parent {
Some((child.to_string(), old))
} else {
None
}
})
.collect();
Ok(changed)
}
fn render_flake(
hyperhive_flake: &str,
nixpkgs_flake: &str,