feat: derive BUN_JSC_forceRAMSize from effective per-agent MemoryMax=

This commit is contained in:
damocles 2026-07-26 21:49:08 +02:00
commit bd14cc5c46
4 changed files with 288 additions and 35 deletions

View file

@ -115,6 +115,91 @@ 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
/// (1024-based), not decimal — this matches. Pure integer arithmetic
/// throughout (via `u128` headroom) rather than `f64`: a `MemoryMax=`
/// 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.
#[must_use]
pub fn parse_bytes(value: &str) -> Option<u64> {
if value == "infinity" || value.ends_with('%') {
return None;
}
let (mantissa, exponent) = [('K', 1u32), ('M', 2), ('G', 3), ('T', 4)]
.into_iter()
.find_map(|(suffix, exp)| {
value
.strip_suffix([suffix, suffix.to_ascii_lowercase()])
.map(|m| (m, exp))
})
.unwrap_or((value, 0));
let scale = u128::from(1024u64.checked_pow(exponent)?);
let (int_part, frac_part) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let whole: u128 = int_part.parse().ok()?;
let mut bytes = whole.checked_mul(scale)?;
if !frac_part.is_empty() {
let frac_num: u128 = frac_part.parse().ok()?;
let frac_denom = 10u128.checked_pow(u32::try_from(frac_part.len()).ok()?)?;
bytes = bytes.checked_add(frac_num.checked_mul(scale)? / frac_denom)?;
}
u64::try_from(bytes).ok()
}
/// Persist the full map. Sorted JSON output keeps meta-repo diffs
/// minimal.
///
@ -323,4 +408,74 @@ mod tests {
assert!(validate_memory_max(v).is_err(), "{v} should be rejected");
}
}
#[test]
fn parse_bytes_handles_plain_and_suffixed_values() {
assert_eq!(parse_bytes("1024"), Some(1024));
assert_eq!(parse_bytes("4G"), Some(4 * 1024 * 1024 * 1024));
assert_eq!(parse_bytes("512M"), Some(512 * 1024 * 1024));
assert_eq!(parse_bytes("4096K"), Some(4096 * 1024));
assert_eq!(parse_bytes("2T"), Some(2 * 1024 * 1024 * 1024 * 1024));
assert_eq!(
parse_bytes("1.5G"),
Some(1024 * 1024 * 1024 + 512 * 1024 * 1024)
);
// Lowercase suffixes accepted, matching `is_byte_size`.
assert_eq!(parse_bytes("4g"), Some(4 * 1024 * 1024 * 1024));
}
#[test]
fn parse_bytes_rejects_infinity_and_percentages() {
assert_eq!(parse_bytes("infinity"), None);
assert_eq!(parse_bytes("50%"), None);
}
#[test]
fn parse_bytes_rejects_garbage() {
for v in ["", "abc", "-8G", "8Gi"] {
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);
}
}