agents: read the memory cap at boot, not from the build
An agent's memory cap was rendered into its flake (`memoryMaxBytes` → `services.hyperhive.agent.claudeMemoryMaxBytes`), so the closure depended on it and a changed cap reached the JSC heap ceiling and the subagent `MemoryHigh=` only after a rebuild. hive-c0re now writes the effective `MemoryMax=` in bytes to the agent's runtime dir (`/run/hive/memory-max-bytes` inside) in `write_dropins`, from the same value the drop-in gets. The file is removed for `infinity` or a RAM percentage, and left alone when the drop-in is kept. Inside the container, `nix/agent-modules/memory-cap.nix` reads it at boot: an environment generator sets `BUN_JSC_forceRAMSize` (75%) for every unit and `machinectl shell`, `/etc/set-environment` exports it in login shells, and a unit generator writes `MemoryHigh=` (2/3) as a drop-in for `hive-subagent-daemon`. The render drops `memoryMaxBytes`, the agent option is removed (both spellings fail eval with a removal message), and `effective_memory_bytes_from` goes with its only caller. Part of #5081 (slice S0-2).
This commit is contained in:
parent
c8df666bf3
commit
0d619e215e
15 changed files with 416 additions and 287 deletions
|
|
@ -122,55 +122,6 @@ fn resolve(limits: &AgentLimits, hive_cpu_quota: &str, hive_memory_max: &str) ->
|
|||
(cpu, mem)
|
||||
}
|
||||
|
||||
/// Effective `MemoryMax=` string for `name` against an already-loaded
|
||||
/// map — the memory-only half of [`effective_from`], split out because
|
||||
/// [`effective_memory_bytes_from`] doesn't have a CPU quota to pass.
|
||||
/// No single-agent convenience wrapper (unlike [`effective`] /
|
||||
/// [`effective_from`]): every current caller already has a loaded map
|
||||
/// in hand, so one would just be dead code.
|
||||
#[must_use]
|
||||
fn effective_memory_max_from(
|
||||
limits: &BTreeMap<String, AgentLimits>,
|
||||
name: &str,
|
||||
hive_memory_max: &str,
|
||||
) -> String {
|
||||
limits
|
||||
.get(name)
|
||||
.and_then(|l| l.memory_max.clone())
|
||||
.unwrap_or_else(|| hive_memory_max.to_owned())
|
||||
}
|
||||
|
||||
/// Effective `MemoryMax=` for `name`, as a raw byte count, against an
|
||||
/// already-loaded map — the form `render_flake_with_lookup` uses so it
|
||||
/// doesn't re-read `resource-limits.json` once per agent (same
|
||||
/// reasoning as [`effective_from`] / `container_view::build_all`). No
|
||||
/// single-agent convenience wrapper, for the same reason as
|
||||
/// [`effective_memory_max_from`] — every current caller already has a
|
||||
/// loaded map in hand.
|
||||
///
|
||||
/// Logs a `warn!` naming `name` when the effective value is a RAM
|
||||
/// percentage: unlike `"infinity"`, a percentage IS a real, resolvable
|
||||
/// cap — resolving one just needs host `MemTotal`, which this module
|
||||
/// doesn't track — so today it degrades to `None` (no derived heap
|
||||
/// ceiling) rather than silently guessing wrong against `MemTotal`.
|
||||
#[must_use]
|
||||
pub fn effective_memory_bytes_from(
|
||||
limits: &BTreeMap<String, AgentLimits>,
|
||||
name: &str,
|
||||
hive_memory_max: &str,
|
||||
) -> Option<u64> {
|
||||
let mem = effective_memory_max_from(limits, name, hive_memory_max);
|
||||
if mem.ends_with('%') {
|
||||
tracing::warn!(
|
||||
agent = name,
|
||||
memory_max = %mem,
|
||||
"effective MemoryMax= is a RAM percentage; can't derive a JSC heap ceiling from it \
|
||||
without host MemTotal — leaving BUN_JSC_forceRAMSize unset for this agent"
|
||||
);
|
||||
}
|
||||
parse_bytes(&mem)
|
||||
}
|
||||
|
||||
/// Parse a systemd `MemoryMax=`-style byte-size value (`"4G"`, `"512M"`,
|
||||
/// a bare byte count, optionally with a decimal like `"1.5G"`) into a
|
||||
/// raw byte count. systemd's `K`/`M`/`G`/`T` suffixes are IEC binary
|
||||
|
|
@ -179,9 +130,8 @@ pub fn effective_memory_bytes_from(
|
|||
/// value is always a small non-negative decimal (enforced by
|
||||
/// [`is_plain_number`] upstream in [`validate_memory_max`]), so floats
|
||||
/// would only add rounding / sign-loss risk for no benefit. Returns
|
||||
/// `None` for `"infinity"` and percentages; see
|
||||
/// [`effective_memory_bytes_from`] for why those can't be turned into a
|
||||
/// byte count here.
|
||||
/// `None` for `"infinity"` and for percentages, which need the host's
|
||||
/// `MemTotal` to resolve.
|
||||
#[must_use]
|
||||
pub fn parse_bytes(value: &str) -> Option<u64> {
|
||||
if value == "infinity" || value.ends_with('%') {
|
||||
|
|
@ -532,46 +482,4 @@ mod tests {
|
|||
assert_eq!(parse_bytes(v), None, "{v} should not parse");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_memory_bytes_falls_back_to_hive_default() {
|
||||
// Empty map, i.e. no per-agent override — exercises the
|
||||
// "use the hive-wide default" arm end to end.
|
||||
let empty = BTreeMap::new();
|
||||
assert_eq!(
|
||||
effective_memory_bytes_from(&empty, "nobody-configured-this-agent", "4G"),
|
||||
Some(4 * 1024 * 1024 * 1024)
|
||||
);
|
||||
assert_eq!(
|
||||
effective_memory_bytes_from(&empty, "nobody-configured-this-agent", "infinity"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_memory_bytes_from_prefers_per_agent_override() {
|
||||
let map = BTreeMap::from([("sock".to_owned(), limits(None, Some("8G")))]);
|
||||
assert_eq!(
|
||||
effective_memory_bytes_from(&map, "sock", HIVE_MEM),
|
||||
Some(8 * 1024 * 1024 * 1024)
|
||||
);
|
||||
// A different agent not in the map still falls back to the
|
||||
// hive-wide default from the same loaded map (no re-read).
|
||||
assert_eq!(
|
||||
effective_memory_bytes_from(&map, "iris", HIVE_MEM),
|
||||
Some(4 * 1024 * 1024 * 1024)
|
||||
);
|
||||
}
|
||||
|
||||
/// A percentage cap is real and resolvable in principle, but this
|
||||
/// module has no host `MemTotal` to resolve it against — must
|
||||
/// degrade to `None` (no derived heap ceiling) rather than silently
|
||||
/// treating it as unbounded or guessing a number. `warn!` firing is
|
||||
/// exercised for coverage but not asserted on (no tracing test
|
||||
/// subscriber wired up here) — the `None` return is the contract.
|
||||
#[test]
|
||||
fn effective_memory_bytes_from_returns_none_for_percentage() {
|
||||
let map = BTreeMap::from([("sock".to_owned(), limits(None, Some("50%")))]);
|
||||
assert_eq!(effective_memory_bytes_from(&map, "sock", HIVE_MEM), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue