hive-c0re: the per-agent resource limit override is gone
The hive no longer sets an agent's CPU or memory cap: caps come from the swarm, else the hive-wide defaults. The container load tab was the only hive UI that set them, and its figures are already exported as OTEL metrics, so with the tab gone nothing wrote meta/resource-limits.json.
Removed: the file's reader and writer, the `hivectl agent set-limits` verb with its host-socket request, the `GET /api/container-resources` and `POST /api/resource-limits/{name}` dashboard endpoints, and the `cpu_quota`/`memory_max` fields of the container view that only the tab read. A leftover resource-limits.json is ignored.
This commit is contained in:
parent
a2540be98b
commit
90e33cbd19
19 changed files with 161 additions and 828 deletions
|
|
@ -27,7 +27,6 @@ pub async fn write_dropins(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Re
|
|||
set_nspawn_flags(&container, &paths.agent, &paths.claude, &paths.notes).await?;
|
||||
if let Some((cpu_quota, memory_max)) = stage_limits(
|
||||
name,
|
||||
&crate::resource_limits::resource_limits_path(),
|
||||
&crate::resource_limits::swarm_limits_path(),
|
||||
&limits_dropin_path(&container),
|
||||
&paths.agent,
|
||||
|
|
@ -97,14 +96,13 @@ impl HostMemory {
|
|||
/// `MemoryMax=` the agent will start under: the new one, or the kept drop-in's.
|
||||
fn stage_limits(
|
||||
agent_name: &str,
|
||||
limits_path: &Path,
|
||||
swarm_path: &Path,
|
||||
dropin_path: &Path,
|
||||
runtime_dir: &Path,
|
||||
hive: &HiveEnv,
|
||||
host: Option<HostMemory>,
|
||||
) -> Result<Option<(String, String)>> {
|
||||
let limits = limits_to_apply(agent_name, limits_path, swarm_path, dropin_path, hive);
|
||||
let limits = limits_to_apply(agent_name, swarm_path, dropin_path, hive);
|
||||
let memory_max = match &limits {
|
||||
Some((_, memory_max)) => Some(memory_max.clone()),
|
||||
None => dropin_memory_max(agent_name, dropin_path),
|
||||
|
|
@ -226,35 +224,27 @@ fn write_memory_cap(runtime_dir: &Path, cap: Option<&str>) -> Result<()> {
|
|||
/// The `(CPUQuota, MemoryMax)` to write into `agent_name`'s limits
|
||||
/// drop-in, or `None` to leave the drop-in at `dropin_path` as it is.
|
||||
///
|
||||
/// When the overrides file at `limits_path` or the swarm caps at
|
||||
/// `swarm_path` can't be read, the error is logged and the agent still starts.
|
||||
/// The last-applied drop-in is kept, because the hive defaults can be looser
|
||||
/// than the agent's own override.
|
||||
/// When the swarm caps at `swarm_path` can't be read, the error is logged and
|
||||
/// the agent still starts. The last-applied drop-in is kept, because the hive
|
||||
/// defaults can be looser than the swarm's cap.
|
||||
/// With no drop-in yet, the agent gets the hive defaults: without one it
|
||||
/// would run uncapped.
|
||||
fn limits_to_apply(
|
||||
agent_name: &str,
|
||||
limits_path: &Path,
|
||||
swarm_path: &Path,
|
||||
dropin_path: &Path,
|
||||
hive: &HiveEnv,
|
||||
) -> Option<(String, String)> {
|
||||
let hive_cpu = &hive.agent_cpu_quota;
|
||||
let hive_mem = &hive.agent_memory_max;
|
||||
let e = match crate::resource_limits::effective_with_swarm(
|
||||
limits_path,
|
||||
swarm_path,
|
||||
agent_name,
|
||||
hive_cpu,
|
||||
hive_mem,
|
||||
) {
|
||||
let e = match crate::resource_limits::effective(swarm_path, agent_name, hive_cpu, hive_mem) {
|
||||
Ok(limits) => return Some(limits),
|
||||
Err(e) => e,
|
||||
};
|
||||
if dropin_path.exists() {
|
||||
tracing::error!(
|
||||
agent = %agent_name,
|
||||
path = %limits_path.display(),
|
||||
path = %swarm_path.display(),
|
||||
error = ?e,
|
||||
"resource limits unreadable — keeping the last-applied limits drop-in"
|
||||
);
|
||||
|
|
@ -262,7 +252,7 @@ fn limits_to_apply(
|
|||
} else {
|
||||
tracing::error!(
|
||||
agent = %agent_name,
|
||||
path = %limits_path.display(),
|
||||
path = %swarm_path.display(),
|
||||
error = ?e,
|
||||
"resource limits unreadable and no limits drop-in yet — applying hive defaults"
|
||||
);
|
||||
|
|
@ -271,14 +261,14 @@ fn limits_to_apply(
|
|||
}
|
||||
|
||||
/// Write a systemd drop-in for `container@<container>.service` that applies
|
||||
/// the agent's effective resource caps — its per-agent overrides from
|
||||
/// `meta/resource-limits.json` where set, the hive-wide defaults
|
||||
/// the agent's effective resource caps — the swarm's cap from
|
||||
/// `meta/swarm-resource-limits.json` where set, the hive-wide defaults
|
||||
/// otherwise. Goes under `/run/systemd/system/...` so it's ephemeral
|
||||
/// (regenerated on every spawn / rebuild, except as [`limits_to_apply`] says).
|
||||
///
|
||||
/// The weights are hive-wide (`services.hyperhive.agentCpuWeight` /
|
||||
/// `agentIoWeight`) — unlike the caps they have no per-agent override in
|
||||
/// `meta/resource-limits.json`, so they come straight off [`HiveEnv`].
|
||||
/// `agentIoWeight`) — unlike the caps they have no swarm cap, so they come
|
||||
/// straight off [`HiveEnv`].
|
||||
/// `None` (the nix option set to `null`) means the weight line is left out
|
||||
/// and the container keeps the kernel default.
|
||||
async fn set_resource_limits(
|
||||
|
|
@ -848,42 +838,35 @@ mod tests {
|
|||
|
||||
const TRUNCATED_LIMITS: &str = "{\n \"sock\": { \"cpu_quota\": \"50%\", \"mem";
|
||||
|
||||
fn absent_swarm(dir: &tempfile::TempDir) -> std::path::PathBuf {
|
||||
dir.path().join("absent-swarm-resource-limits.json")
|
||||
fn swarm_file(dir: &tempfile::TempDir) -> std::path::PathBuf {
|
||||
dir.path().join("swarm-resource-limits.json")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_limits_keep_an_existing_dropin() {
|
||||
fn corrupt_swarm_caps_keep_an_existing_dropin() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
let swarm = swarm_file(&dir);
|
||||
let dropin = dir.path().join("hyperhive-limits.conf");
|
||||
std::fs::write(&limits, TRUNCATED_LIMITS).expect("seed limits");
|
||||
std::fs::write(&swarm, TRUNCATED_LIMITS).expect("seed swarm caps");
|
||||
std::fs::write(&dropin, "MemoryMax=1G\n").expect("seed drop-in");
|
||||
assert_eq!(
|
||||
limits_to_apply(
|
||||
"sock",
|
||||
&limits,
|
||||
&absent_swarm(&dir),
|
||||
&dropin,
|
||||
&HiveEnv::default()
|
||||
),
|
||||
limits_to_apply("sock", &swarm, &dropin, &HiveEnv::default()),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(&limits).expect("read"),
|
||||
std::fs::read(&swarm).expect("read"),
|
||||
TRUNCATED_LIMITS.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_limits_without_a_dropin_apply_hive_defaults() {
|
||||
fn corrupt_swarm_caps_without_a_dropin_apply_hive_defaults() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
std::fs::write(&limits, TRUNCATED_LIMITS).expect("seed limits");
|
||||
let swarm = swarm_file(&dir);
|
||||
std::fs::write(&swarm, TRUNCATED_LIMITS).expect("seed swarm caps");
|
||||
let applied = limits_to_apply(
|
||||
"sock",
|
||||
&limits,
|
||||
&absent_swarm(&dir),
|
||||
&swarm,
|
||||
&dir.path().join("absent.conf"),
|
||||
&HiveEnv::default(),
|
||||
);
|
||||
|
|
@ -891,32 +874,12 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_swarm_caps_keep_an_existing_dropin() {
|
||||
fn a_swarm_cap_is_applied_over_the_hive_default() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let swarm = dir.path().join("swarm-resource-limits.json");
|
||||
let dropin = dir.path().join("hyperhive-limits.conf");
|
||||
std::fs::write(&swarm, TRUNCATED_LIMITS).expect("seed swarm caps");
|
||||
std::fs::write(&dropin, "MemoryMax=1G\n").expect("seed drop-in");
|
||||
let applied = limits_to_apply(
|
||||
"sock",
|
||||
&dir.path().join("resource-limits.json"),
|
||||
&swarm,
|
||||
&dropin,
|
||||
&HiveEnv::default(),
|
||||
);
|
||||
assert_eq!(applied, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_swarm_cap_is_applied_over_the_operator_override() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
let swarm = dir.path().join("swarm-resource-limits.json");
|
||||
std::fs::write(&limits, r#"{"sock": {"memory_max": "16G"}}"#).expect("seed limits");
|
||||
let swarm = swarm_file(&dir);
|
||||
std::fs::write(&swarm, r#"{"sock": {"memory_max": "2G"}}"#).expect("seed swarm caps");
|
||||
let applied = limits_to_apply(
|
||||
"sock",
|
||||
&limits,
|
||||
&swarm,
|
||||
&dir.path().join("absent.conf"),
|
||||
&HiveEnv::default(),
|
||||
|
|
@ -925,19 +888,13 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn readable_limits_are_applied_over_an_existing_dropin() {
|
||||
fn readable_swarm_caps_are_applied_over_an_existing_dropin() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
let swarm = swarm_file(&dir);
|
||||
let dropin = dir.path().join("hyperhive-limits.conf");
|
||||
std::fs::write(&limits, r#"{"sock": {"memory_max": "1G"}}"#).expect("seed limits");
|
||||
std::fs::write(&swarm, r#"{"sock": {"memory_max": "1G"}}"#).expect("seed swarm caps");
|
||||
std::fs::write(&dropin, "MemoryMax=8G\n").expect("seed drop-in");
|
||||
let applied = limits_to_apply(
|
||||
"sock",
|
||||
&limits,
|
||||
&absent_swarm(&dir),
|
||||
&dropin,
|
||||
&HiveEnv::default(),
|
||||
);
|
||||
let applied = limits_to_apply("sock", &swarm, &dropin, &HiveEnv::default());
|
||||
assert_eq!(applied, Some(("200%".to_owned(), "1G".to_owned())));
|
||||
}
|
||||
|
||||
|
|
@ -953,8 +910,7 @@ mod tests {
|
|||
let runtime = dir.path().join("runtime");
|
||||
let applied = stage_limits(
|
||||
"sock",
|
||||
&dir.path().join("resource-limits.json"),
|
||||
&dir.path().join("swarm-resource-limits.json"),
|
||||
&swarm_file(dir),
|
||||
&dir.path().join("hyperhive-limits.conf"),
|
||||
&runtime,
|
||||
&HiveEnv::default(),
|
||||
|
|
@ -965,40 +921,18 @@ mod tests {
|
|||
(applied.map(|(_, mem)| mem), copy)
|
||||
}
|
||||
|
||||
/// The agent's copy follows the swarm's cap, the one systemd enforces, over
|
||||
/// the operator's override: a heap sized against the override would
|
||||
/// outgrow the cgroup.
|
||||
#[test]
|
||||
fn the_agent_reads_the_swarm_cap_over_the_operator_override() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
std::fs::write(
|
||||
dir.path().join("resource-limits.json"),
|
||||
r#"{"sock": {"memory_max": "16G"}}"#,
|
||||
)
|
||||
.expect("seed limits");
|
||||
std::fs::write(
|
||||
dir.path().join("swarm-resource-limits.json"),
|
||||
r#"{"sock": {"memory_max": "2G"}}"#,
|
||||
)
|
||||
.expect("seed swarm caps");
|
||||
assert_eq!(
|
||||
stage(&dir),
|
||||
(Some("2G".to_owned()), Some("2147483648\n".to_owned()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_agent_reads_the_cap_its_dropin_carries() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
let swarm = swarm_file(&dir);
|
||||
|
||||
// No overrides anywhere: the hive-wide default, 4G.
|
||||
// No swarm cap anywhere: the hive-wide default, 4G.
|
||||
assert_eq!(
|
||||
stage(&dir),
|
||||
(Some("4G".to_owned()), Some("4294967296\n".to_owned()))
|
||||
);
|
||||
|
||||
std::fs::write(&limits, r#"{"sock": {"memory_max": "16G"}}"#).expect("seed limits");
|
||||
std::fs::write(&swarm, r#"{"sock": {"memory_max": "16G"}}"#).expect("seed swarm caps");
|
||||
assert_eq!(
|
||||
stage(&dir),
|
||||
(Some("16G".to_owned()), Some("17179869184\n".to_owned()))
|
||||
|
|
@ -1008,17 +942,17 @@ mod tests {
|
|||
#[test]
|
||||
fn every_kind_of_cap_is_written_for_the_agent() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
let swarm = swarm_file(&dir);
|
||||
for (memory_max, copy) in [
|
||||
("infinity", "infinity\n"),
|
||||
("50%", "8192004096\n"),
|
||||
("8G", "8589934592\n"),
|
||||
] {
|
||||
std::fs::write(
|
||||
&limits,
|
||||
&swarm,
|
||||
format!(r#"{{"sock": {{"memory_max": "{memory_max}"}}}}"#),
|
||||
)
|
||||
.expect("seed limits");
|
||||
.expect("seed swarm caps");
|
||||
assert_eq!(
|
||||
stage(&dir),
|
||||
(Some(memory_max.to_owned()), Some(copy.to_owned())),
|
||||
|
|
@ -1072,14 +1006,14 @@ mod tests {
|
|||
#[test]
|
||||
fn a_kept_dropin_hands_the_agent_its_cap() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
let swarm = swarm_file(&dir);
|
||||
let dropin = dir.path().join("hyperhive-limits.conf");
|
||||
let copy = dir.path().join("runtime").join(MEMORY_CAP_FILE);
|
||||
std::fs::write(&limits, r#"{"sock": {"memory_max": "1G"}}"#).expect("seed limits");
|
||||
std::fs::write(&swarm, r#"{"sock": {"memory_max": "1G"}}"#).expect("seed swarm caps");
|
||||
assert_eq!(stage(&dir).1, Some("1073741824\n".to_owned()));
|
||||
|
||||
// Unreadable limits keep the drop-in; the copy follows the drop-in.
|
||||
std::fs::write(&limits, TRUNCATED_LIMITS).expect("seed limits");
|
||||
// Unreadable caps keep the drop-in; the copy follows the drop-in.
|
||||
std::fs::write(&swarm, TRUNCATED_LIMITS).expect("seed swarm caps");
|
||||
std::fs::write(
|
||||
&dropin,
|
||||
"[Unit]\nConditionPathIsDirectory=/rt/sock\n\n[Service]\nRestartSec=5\nMemoryMax=2G\nCPUQuota=200%\n",
|
||||
|
|
@ -1103,29 +1037,21 @@ mod tests {
|
|||
#[test]
|
||||
fn an_unknown_cap_leaves_no_copy() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
let swarm = swarm_file(&dir);
|
||||
let dropin = dir.path().join("hyperhive-limits.conf");
|
||||
assert_eq!(stage(&dir).1, Some("4294967296\n".to_owned()));
|
||||
|
||||
// Unreadable limits and an unreadable drop-in: nothing says what the
|
||||
// Unreadable caps and an unreadable drop-in: nothing says what the
|
||||
// cgroup enforces.
|
||||
std::fs::write(&limits, TRUNCATED_LIMITS).expect("seed limits");
|
||||
std::fs::write(&swarm, TRUNCATED_LIMITS).expect("seed swarm caps");
|
||||
std::fs::create_dir(&dropin).expect("seed unreadable drop-in");
|
||||
assert_eq!(stage(&dir), (None, None));
|
||||
|
||||
// A percentage with no host memory to resolve it against.
|
||||
std::fs::write(&limits, r#"{"sock": {"memory_max": "50%"}}"#).expect("seed limits");
|
||||
std::fs::write(&swarm, r#"{"sock": {"memory_max": "50%"}}"#).expect("seed swarm caps");
|
||||
let runtime = dir.path().join("runtime");
|
||||
let applied = stage_limits(
|
||||
"sock",
|
||||
&limits,
|
||||
&dir.path().join("swarm-resource-limits.json"),
|
||||
&dropin,
|
||||
&runtime,
|
||||
&HiveEnv::default(),
|
||||
None,
|
||||
)
|
||||
.expect("stage limits");
|
||||
let applied = stage_limits("sock", &swarm, &dropin, &runtime, &HiveEnv::default(), None)
|
||||
.expect("stage limits");
|
||||
assert_eq!(applied.map(|(_, mem)| mem), Some("50%".to_owned()));
|
||||
assert!(!runtime.join(MEMORY_CAP_FILE).exists());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue