agents: hand over the kept drop-in's cap, warn at boot without one
When hive-c0re can't read resource-limits.json and a limits drop-in already exists, it keeps the drop-in, but it wrote no memory cap file. A container whose runtime dir had no file yet (hive-c0re upgraded, /run not cleared) then started with no heap ceiling and no subagent MemoryHigh=, silently. `stage_limits` now writes the file on every call: from the new limits, or from the last `MemoryMax=` in the kept drop-in (`infinity` when it has none or can't be read). A missing or malformed cap file at boot is no longer silent. The generators run before the container's journal and an nspawn container has no /dev/kmsg, so a oneshot `hyperhive-memory-cap-check.service` logs a warning naming the path. The agent then runs with no JSC heap ceiling and no subagent MemoryHigh=; the container's MemoryMax= still applies. module-eval-agent-memory gains a case pinning how the ceiling reaches claude: `hive-agent` and `hive-subagent-daemon` neither set nor unset BUN_JSC_forceRAMSize, both generators and the check unit are installed. Part of #5081 (slice S0-2).
This commit is contained in:
parent
0d619e215e
commit
57955a6c53
6 changed files with 144 additions and 32 deletions
|
|
@ -61,8 +61,8 @@ const MEMORY_CAP_FILE: &str = "memory-max-bytes";
|
|||
/// [`limits_to_apply`], plus handing the memory cap to the agent through
|
||||
/// [`MEMORY_CAP_FILE`] in `runtime_dir`. The container cannot read the cap
|
||||
/// systemd enforces on it, so this file is how it learns the value it sizes
|
||||
/// its heap and its subagents against. When the drop-in is kept as it is, so is
|
||||
/// the file.
|
||||
/// its heap and its subagents against. It is written on every call, from the
|
||||
/// `MemoryMax=` the agent will start under: the new one, or the kept drop-in's.
|
||||
fn stage_limits(
|
||||
agent_name: &str,
|
||||
limits_path: &Path,
|
||||
|
|
@ -71,12 +71,39 @@ fn stage_limits(
|
|||
hive: &HiveEnv,
|
||||
) -> Result<Option<(String, String)>> {
|
||||
let limits = limits_to_apply(agent_name, limits_path, dropin_path, hive);
|
||||
if let Some((_, memory_max)) = &limits {
|
||||
write_memory_cap(runtime_dir, agent_name, memory_max)?;
|
||||
}
|
||||
let memory_max = match &limits {
|
||||
Some((_, memory_max)) => memory_max.clone(),
|
||||
None => dropin_memory_max(agent_name, dropin_path),
|
||||
};
|
||||
write_memory_cap(runtime_dir, agent_name, &memory_max)?;
|
||||
Ok(limits)
|
||||
}
|
||||
|
||||
/// The `MemoryMax=` the drop-in at `path` applies: its last assignment, as
|
||||
/// systemd reads it. `infinity`, systemd's default, when it has none, its last
|
||||
/// one is empty, or the file can't be read.
|
||||
fn dropin_memory_max(agent_name: &str, path: &Path) -> String {
|
||||
let body = match std::fs::read_to_string(path) {
|
||||
Ok(body) => body,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
agent = %agent_name,
|
||||
path = %path.display(),
|
||||
error = ?e,
|
||||
"limits drop-in unreadable; the agent gets no memory cap byte count"
|
||||
);
|
||||
return "infinity".to_owned();
|
||||
}
|
||||
};
|
||||
body.lines()
|
||||
.rev()
|
||||
.find_map(|line| line.trim().strip_prefix("MemoryMax="))
|
||||
.map(str::trim)
|
||||
.filter(|v| !v.is_empty())
|
||||
.unwrap_or("infinity")
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Write `memory_max` into [`MEMORY_CAP_FILE`] as a byte count, or remove the
|
||||
/// file when the value has none: `infinity`, or a RAM percentage, which needs
|
||||
/// the host's `MemTotal` to resolve. With no file the agent sets no heap or
|
||||
|
|
@ -815,15 +842,33 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn a_kept_dropin_keeps_the_agents_copy() {
|
||||
fn a_kept_dropin_hands_the_agent_its_cap() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let limits = dir.path().join("resource-limits.json");
|
||||
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");
|
||||
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");
|
||||
std::fs::write(dir.path().join("hyperhive-limits.conf"), "MemoryMax=1G\n")
|
||||
.expect("seed drop-in");
|
||||
assert_eq!(stage(&dir), (None, Some("1073741824\n".to_owned())));
|
||||
std::fs::write(
|
||||
&dropin,
|
||||
"[Unit]\nConditionPathIsDirectory=/rt/sock\n\n[Service]\nRestartSec=5\nMemoryMax=2G\nCPUQuota=200%\n",
|
||||
)
|
||||
.expect("seed drop-in");
|
||||
assert_eq!(stage(&dir), (None, Some("2147483648\n".to_owned())));
|
||||
|
||||
// A kept drop-in with no copy beside it, as after an upgrade.
|
||||
std::fs::remove_file(©).expect("remove copy");
|
||||
assert_eq!(stage(&dir), (None, Some("2147483648\n".to_owned())));
|
||||
|
||||
// The last assignment is the one systemd applies.
|
||||
std::fs::write(&dropin, "[Service]\nMemoryMax=2G\nMemoryMax=3G\n").expect("seed drop-in");
|
||||
assert_eq!(stage(&dir), (None, Some("3221225472\n".to_owned())));
|
||||
|
||||
// No `MemoryMax=` is systemd's default, `infinity`: no byte count.
|
||||
std::fs::write(&dropin, "[Service]\nCPUQuota=200%\n").expect("seed drop-in");
|
||||
assert_eq!(stage(&dir), (None, None));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue