Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89b4e8afb6 | ||
|
|
877e8bcf81 |
2 changed files with 110 additions and 11 deletions
|
|
@ -56,9 +56,9 @@ pub(super) struct StateSnapshot {
|
|||
/// survive after a destroy-without-purge. The operator can re-spawn
|
||||
/// with the same name to resume, or PURG3 to wipe them.
|
||||
tombstones: Vec<TombstoneView>,
|
||||
/// Sub-agents whose FNV-1a hashed web UI port collides with at
|
||||
/// least one other agent. Operator resolves by renaming. The
|
||||
/// dashboard renders a banner at the top listing each cluster.
|
||||
/// Agents whose FNV-1a hashed web UI port collides with at least
|
||||
/// one other. Operator resolves by renaming. The dashboard renders
|
||||
/// a banner at the top listing each cluster.
|
||||
port_conflicts: Vec<PortConflict>,
|
||||
/// Inputs in `meta/flake.lock` the operator can selectively
|
||||
/// `nix flake update`. Hyperhive first, then `agent-<n>` rows.
|
||||
|
|
@ -342,11 +342,13 @@ pub(super) async fn api_state(
|
|||
})
|
||||
}
|
||||
|
||||
/// Group live containers by their assigned web UI port; clusters with
|
||||
/// more than one member are port-hash collisions the operator needs
|
||||
/// to resolve by renaming. Manager (fixed at 8000) and sub-agents
|
||||
/// (8100..8999) can't collide with each other — collisions are
|
||||
/// strictly between sub-agents.
|
||||
/// Group containers by their assigned web UI port; clusters with more
|
||||
/// than one member are port-hash collisions the operator resolves by
|
||||
/// renaming an agent.
|
||||
///
|
||||
/// Every container's port comes from [`crate::lifecycle::agent_web_port`],
|
||||
/// which hashes the name into one range for all of them — so the manager
|
||||
/// is a candidate for a collision exactly like any sub-agent.
|
||||
fn build_port_conflicts(containers: &[ContainerView]) -> Vec<PortConflict> {
|
||||
let mut by_port: std::collections::BTreeMap<u16, Vec<String>> =
|
||||
std::collections::BTreeMap::new();
|
||||
|
|
@ -712,3 +714,100 @@ pub(super) async fn dashboard_stream(
|
|||
});
|
||||
Sse::new(stream).keep_alive(KeepAlive::default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ContainerView, build_port_conflicts};
|
||||
|
||||
fn cv(name: &str, port: u16, running: bool) -> ContainerView {
|
||||
ContainerView {
|
||||
name: name.to_owned(),
|
||||
container: format!("h-{name}"),
|
||||
port,
|
||||
running,
|
||||
failed: false,
|
||||
needs_update: false,
|
||||
needs_login: false,
|
||||
deployed_sha: None,
|
||||
parent: None,
|
||||
active_model: None,
|
||||
paused: false,
|
||||
cpu_quota: "200%".to_owned(),
|
||||
memory_max: "4G".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distinct_ports_produce_no_conflicts() {
|
||||
let cs = [cv("alice", 8101, true), cv("bob", 8102, true)];
|
||||
assert!(build_port_conflicts(&cs).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lone_container_does_not_conflict_with_itself() {
|
||||
assert!(build_port_conflicts(&[cv("alice", 8101, true)]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_agents_on_one_port_are_reported_together() {
|
||||
let cs = [cv("alice", 8101, true), cv("bob", 8101, true)];
|
||||
let got = build_port_conflicts(&cs);
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].port, 8101);
|
||||
assert_eq!(got[0].agents, ["alice", "bob"]);
|
||||
}
|
||||
|
||||
/// The manager takes its port from the same hash as everyone else, so
|
||||
/// it collides like any other name. A doc comment here used to claim it
|
||||
/// sat at a fixed 8000 and could not — no such special case exists.
|
||||
#[test]
|
||||
fn the_manager_can_collide_with_a_sub_agent() {
|
||||
let cs = [cv("manager", 8101, true), cv("alice", 8101, true)];
|
||||
let got = build_port_conflicts(&cs);
|
||||
assert_eq!(got.len(), 1);
|
||||
assert_eq!(got[0].agents, ["alice", "manager"]);
|
||||
}
|
||||
|
||||
/// A stopped agent keeps its hashed port, so the clash is real the
|
||||
/// moment it starts. Reporting it while it is down is the point.
|
||||
#[test]
|
||||
fn a_stopped_container_still_claims_its_port() {
|
||||
let cs = [cv("alice", 8101, true), cv("bob", 8101, false)];
|
||||
assert_eq!(build_port_conflicts(&cs).len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_are_sorted_within_a_conflict() {
|
||||
let cs = [cv("zoe", 8101, true), cv("adam", 8101, true)];
|
||||
assert_eq!(build_port_conflicts(&cs)[0].agents, ["adam", "zoe"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_than_two_agents_land_in_one_cluster() {
|
||||
let cs = [
|
||||
cv("c", 8101, true),
|
||||
cv("a", 8101, true),
|
||||
cv("b", 8101, true),
|
||||
];
|
||||
let got = build_port_conflicts(&cs);
|
||||
assert_eq!(got.len(), 1, "one cluster, not one row per pair");
|
||||
assert_eq!(got[0].agents, ["a", "b", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separate_clusters_are_ordered_by_port() {
|
||||
let cs = [
|
||||
cv("d", 8300, true),
|
||||
cv("c", 8300, true),
|
||||
cv("b", 8200, true),
|
||||
cv("a", 8200, true),
|
||||
];
|
||||
let ports: Vec<u16> = build_port_conflicts(&cs).iter().map(|p| p.port).collect();
|
||||
assert_eq!(ports, [8200, 8300]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_containers_is_not_a_conflict() {
|
||||
assert!(build_port_conflicts(&[]).is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,9 +61,9 @@ pub fn container_claude_mount(name: &str) -> String {
|
|||
/// willing to lose (other agents may delete them).
|
||||
pub const CONTAINER_SHARED_MOUNT: &str = "/shared";
|
||||
|
||||
/// Sub-agent web UI port range. Deterministic from the agent's name (FNV-1a
|
||||
/// hash mod range size), so the dashboard can compute the same port without
|
||||
/// asking hive-c0re.
|
||||
/// Web UI port range, shared by every agent including the manager.
|
||||
/// Deterministic from the agent's name (FNV-1a hash mod range size), so the
|
||||
/// dashboard can compute the same port without asking hive-c0re.
|
||||
const WEB_PORT_BASE: u16 = 8100;
|
||||
const WEB_PORT_RANGE: u16 = 900;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue