Watch
0
0
Fork
You've already forked hyperhive
0

hive-c0re: let a swarm cap replace the hive's answer, not join its chain

mara's ruling: "swarm level overrides replace the hive ui limits. otherwise
dont change the logic."

  swarm value present  ->  it is the value; nothing hive-side is consulted
  swarm value absent   ->  exactly today's chain: per-agent field, else hive

Per field, so a swarm memory ceiling leaves CPU on the hive's chain.

The fallback chain itself is not touched: effective_from_with_swarm calls
effective_from as it was and lets the swarm replace its result, and
effective_with_swarm delegates to effective, so there stays one place the
chain lives. `an_empty_swarm_map_leaves_the_existing_chain_bit_for_bit`
asserts the wrapper is a no-op across every shape the chain distinguishes.

The swarm's caps live in their own swarm-resource-limits.json, not in the
operator's resource-limits.json: different writers, different lifetimes,
and a swarm write into the operator's file would make "reset to hive
defaults" destroy something the operator never set. It is read and written
through the same read_map / write_map, so an unreadable file is an error,
and write_dropins resolves both files through limits_to_apply, whose
keep-the-last-applied-drop-in policy covers the swarm file too.

stage_limits hands limits_to_apply the swarm caps file too, so the
agent's memory-max-bytes copy, which it sizes its heap and subagents
against, is the MemoryMax= systemd enforces: the swarm cap when there is
one.

Nothing writes swarm-resource-limits.json yet; the reader comes with the
swarm's agent-limits bucket later in this series.
This commit is contained in:
atlas 2026-10-12 11:26:18 +02:00 • committed by mara
commit 37f6a845a0
4 changed files with 232 additions and 9 deletions

View file

@ -28,6 +28,7 @@ pub async fn write_dropins(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Re
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,
hive,
@ -97,12 +98,13 @@ impl HostMemory {
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, dropin_path, hive);
let limits = limits_to_apply(agent_name, limits_path, 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),
@ -224,20 +226,28 @@ 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` 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 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.
/// 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(limits_path, agent_name, hive_cpu, hive_mem) {
let e = match crate::resource_limits::effective_with_swarm(
limits_path,
swarm_path,
agent_name,
hive_cpu,
hive_mem,
) {
Ok(limits) => return Some(limits),
Err(e) => e,
};
@ -838,6 +848,10 @@ 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")
}
#[test]
fn corrupt_limits_keep_an_existing_dropin() {
let dir = tempfile::tempdir().expect("tempdir");
@ -846,7 +860,13 @@ mod tests {
std::fs::write(&limits, TRUNCATED_LIMITS).expect("seed limits");
std::fs::write(&dropin, "MemoryMax=1G\n").expect("seed drop-in");
assert_eq!(
limits_to_apply("sock", &limits, &dropin, &HiveEnv::default()),
limits_to_apply(
"sock",
&limits,
&absent_swarm(&dir),
&dropin,
&HiveEnv::default()
),
None
);
assert_eq!(
@ -863,12 +883,47 @@ mod tests {
let applied = limits_to_apply(
"sock",
&limits,
&absent_swarm(&dir),
&dir.path().join("absent.conf"),
&HiveEnv::default(),
);
assert_eq!(applied, Some(("200%".to_owned(), "4G".to_owned())));
}
#[test]
fn corrupt_swarm_caps_keep_an_existing_dropin() {
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");
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(),
);
assert_eq!(applied, Some(("200%".to_owned(), "2G".to_owned())));
}
#[test]
fn readable_limits_are_applied_over_an_existing_dropin() {
let dir = tempfile::tempdir().expect("tempdir");
@ -876,7 +931,13 @@ mod tests {
let dropin = dir.path().join("hyperhive-limits.conf");
std::fs::write(&limits, r#"{"sock": {"memory_max": "1G"}}"#).expect("seed limits");
std::fs::write(&dropin, "MemoryMax=8G\n").expect("seed drop-in");
let applied = limits_to_apply("sock", &limits, &dropin, &HiveEnv::default());
let applied = limits_to_apply(
"sock",
&limits,
&absent_swarm(&dir),
&dropin,
&HiveEnv::default(),
);
assert_eq!(applied, Some(("200%".to_owned(), "1G".to_owned())));
}
@ -893,6 +954,7 @@ mod tests {
let applied = stage_limits(
"sock",
&dir.path().join("resource-limits.json"),
&dir.path().join("swarm-resource-limits.json"),
&dir.path().join("hyperhive-limits.conf"),
&runtime,
&HiveEnv::default(),
@ -903,6 +965,28 @@ 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");
@ -1035,6 +1119,7 @@ mod tests {
let applied = stage_limits(
"sock",
&limits,
&dir.path().join("swarm-resource-limits.json"),
&dropin,
&runtime,
&HiveEnv::default(),