diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index a53f6901..c08927df 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -87,12 +87,14 @@ pub struct Coordinator { pub agent_memory_max: String, /// Per-agent systemd `CPUWeight=` (cgroup v2 `cpu.weight`, 1..=10000). /// Same drop-in. A *relative share* under contention, not a cap — see - /// the `agentCpuWeight` NixOS option for the full semantics. - pub agent_cpu_weight: u32, + /// the `agentCpuWeight` NixOS option for the full semantics. `None` + /// (option set to `null`) omits the setting: kernel default, and the + /// drop-in is byte-identical to the one written before weights existed. + pub agent_cpu_weight: Option, /// Per-agent systemd `IOWeight=` (cgroup v2 `io.weight`, 1..=10000). - /// Same drop-in, same relative-share semantics as + /// Same drop-in, same relative-share and `None` semantics as /// [`Self::agent_cpu_weight`]. - pub agent_io_weight: u32, + pub agent_io_weight: Option, /// Operator-tunable model→price table backing the hive-wide cost /// estimate on the ST4TS tab. Set via `services.hyperhive.modelPrices` /// and passed to `hive-c0re serve --model-prices `. Models not @@ -220,10 +222,11 @@ pub struct HiveEnv { pub agent_memory_max: String, /// Per-agent systemd `CPUWeight=` — cgroup v2 `cpu.weight`, 1..=10000. /// Relative share under contention, not a cap; see `agentCpuWeight`. - pub agent_cpu_weight: u32, + /// `None` = leave the setting out of the drop-in (kernel default). + pub agent_cpu_weight: Option, /// Per-agent systemd `IOWeight=` — cgroup v2 `io.weight`, 1..=10000. /// Same semantics as [`Self::agent_cpu_weight`]; see `agentIoWeight`. - pub agent_io_weight: u32, + pub agent_io_weight: Option, } impl Default for HiveEnv { @@ -244,8 +247,8 @@ impl Default for HiveEnv { // Slightly below the kernel default of 100, so agent // containers yield to host services + the infra containers // (which carry no drop-in and stay at 100) under contention. - agent_cpu_weight: 80, - agent_io_weight: 80, + agent_cpu_weight: Some(80), + agent_io_weight: Some(80), } } } diff --git a/hive-c0re/src/lifecycle/host_config.rs b/hive-c0re/src/lifecycle/host_config.rs index b6795dbe..1574f1a5 100644 --- a/hive-c0re/src/lifecycle/host_config.rs +++ b/hive-c0re/src/lifecycle/host_config.rs @@ -45,12 +45,14 @@ pub async fn write_dropins(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Re /// 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`]. +/// `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( container: &str, cpu_quota: &str, memory_max: &str, - cpu_weight: u32, - io_weight: u32, + cpu_weight: Option, + io_weight: Option, ) -> Result<()> { crate::priv_client::write_resource_limits( container, memory_max, cpu_quota, cpu_weight, io_weight, diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 351df29b..c6264986 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -197,11 +197,15 @@ async fn main() -> Result<()> { if let Some(v) = agent_memory_max { sc.env.agent_memory_max = v; } + // Passing the flag sets a weight; omitting it keeps whatever the + // config file says (including `null` = don't emit the setting). + // There's deliberately no flag spelling for "clear it" — that's + // what the nix option's `null` is for. if let Some(v) = agent_cpu_weight { - sc.env.agent_cpu_weight = v; + sc.env.agent_cpu_weight = Some(v); } if let Some(v) = agent_io_weight { - sc.env.agent_io_weight = v; + sc.env.agent_io_weight = Some(v); } if let Some(v) = model_prices { sc.model_prices = diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs index 8059ff56..2ce0020a 100644 --- a/hive-c0re/src/priv_client.rs +++ b/hive-c0re/src/priv_client.rs @@ -174,8 +174,8 @@ pub async fn write_resource_limits( container: &str, memory_max: &str, cpu_quota: &str, - cpu_weight: u32, - io_weight: u32, + cpu_weight: Option, + io_weight: Option, ) -> Result<()> { ok(call(&PrivRequest::WriteResourceLimits { container: container.to_owned(), diff --git a/hive-priv-sock/src/lib.rs b/hive-priv-sock/src/lib.rs index 979bc112..fcb71318 100644 --- a/hive-priv-sock/src/lib.rs +++ b/hive-priv-sock/src/lib.rs @@ -343,15 +343,17 @@ pub enum PrivRequest { container: String, memory_max: String, cpu_quota: String, - /// cgroup v2 `cpu.weight`, 1..=10000. `#[serde(default)]` so a - /// hive-priv built before this field still deserialises new - /// requests; 0 is treated as "omit the line" by the writer. + /// cgroup v2 `cpu.weight`, 1..=10000. `None` means "not + /// configured" — the writer omits the line entirely, leaving the + /// kernel default. `#[serde(default)]` so a request from a + /// hive-c0re built before this field existed deserialises to + /// `None` and reproduces the pre-weights drop-in. #[serde(default)] - cpu_weight: u32, - /// cgroup v2 `io.weight`, 1..=10000. Same default/omit rule as + cpu_weight: Option, + /// cgroup v2 `io.weight`, 1..=10000. Same `None` = omit rule as /// `cpu_weight`. #[serde(default)] - io_weight: u32, + io_weight: Option, }, /// Remove `/run/systemd/system/container@.service.d/` if present. diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 41e900a2..a9e150fb 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -537,8 +537,8 @@ fn chmod_socket_dir(agent_name: &str, mode: u32) -> Result<(String, String)> { /// `CPUQuota=` are hard caps that throttle even on an idle host; /// `CPUWeight=` / `IOWeight=` are cgroup v2 relative shares that only /// decide who yields *under contention*. A zero weight means "not -/// configured" and omits the line, so a hive-c0re built before the weights -/// existed keeps producing the old two-line drop-in. +/// configured" (`None`) and omits the line, so a hive-c0re built before the +/// weights existed keeps producing the old two-line drop-in. /// /// The condition causes systemd to *skip* (not *fail*) the unit when the /// bind-mount source dir is absent — result is `condition`, which does not @@ -550,8 +550,8 @@ fn write_resource_limits( container: &str, memory_max: &str, cpu_quota: &str, - cpu_weight: u32, - io_weight: u32, + cpu_weight: Option, + io_weight: Option, ) -> Result<(String, String)> { validate_container_system_name(container)?; // Derive the logical agent name (strip h- prefix) to form the runtime @@ -572,29 +572,21 @@ fn write_resource_limits( /// `[Unit]`: the condition is checked at start time — it skips (not fails) /// the unit when the MCP socket dir is absent, avoiding restart loops. /// `[Service]`: the hard caps first, then the relative weights. A weight of -/// `0` means "not configured" and omits its line entirely, so a request from -/// a hive-c0re built before the weights existed reproduces the old -/// two-setting drop-in byte for byte. +/// `None` means "not configured" and omits its line entirely, so a request +/// from a hive-c0re built before the weights existed — or one whose nix +/// option is `null` — reproduces the old two-setting drop-in byte for byte. fn limits_dropin_body( runtime_dir: &str, memory_max: &str, cpu_quota: &str, - cpu_weight: u32, - io_weight: u32, + cpu_weight: Option, + io_weight: Option, ) -> String { // Built as two possibly-empty lines rather than pushed onto the // string: `format!` appended to a `String` trips clippy::pedantic's // `format_push_string`, and a `write!` would need an unwrap. - let cpu_weight_line = if cpu_weight > 0 { - format!("CPUWeight={cpu_weight}\n") - } else { - String::new() - }; - let io_weight_line = if io_weight > 0 { - format!("IOWeight={io_weight}\n") - } else { - String::new() - }; + let cpu_weight_line = cpu_weight.map_or_else(String::new, |w| format!("CPUWeight={w}\n")); + let io_weight_line = io_weight.map_or_else(String::new, |w| format!("IOWeight={w}\n")); format!( "[Unit]\n\ ConditionPathIsDirectory={runtime_dir}\n\ @@ -2231,13 +2223,13 @@ mod tests { use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; - /// A zero weight is "not configured": the drop-in must come out + /// An unset weight is "not configured": the drop-in must come out /// byte-identical to the pre-weights two-setting body, so a hive-c0re /// older than this field can't change what lands on disk. #[test] - fn zero_weights_reproduce_the_pre_weights_dropin() { + fn unset_weights_reproduce_the_pre_weights_dropin() { assert_eq!( - limits_dropin_body("/run/hyperhive/agents/iris", "4G", "200%", 0, 0), + limits_dropin_body("/run/hyperhive/agents/iris", "4G", "200%", None, None), "[Unit]\n\ ConditionPathIsDirectory=/run/hyperhive/agents/iris\n\ \n\ @@ -2248,23 +2240,23 @@ mod tests { } /// Weights are appended to the `[Service]` section, each omitted - /// independently when zero. + /// independently when `None`. #[test] fn weights_are_emitted_only_when_set() { - let both = limits_dropin_body("/rt/x", "4G", "200%", 80, 80); + let both = limits_dropin_body("/rt/x", "4G", "200%", Some(80), Some(80)); assert!( both.ends_with("CPUQuota=200%\nCPUWeight=80\nIOWeight=80\n"), "{both}" ); - let cpu_only = limits_dropin_body("/rt/x", "4G", "200%", 80, 0); + let cpu_only = limits_dropin_body("/rt/x", "4G", "200%", Some(80), None); assert!( cpu_only.ends_with("CPUQuota=200%\nCPUWeight=80\n"), "{cpu_only}" ); assert!(!cpu_only.contains("IOWeight"), "{cpu_only}"); - let io_only = limits_dropin_body("/rt/x", "4G", "200%", 0, 80); + let io_only = limits_dropin_body("/rt/x", "4G", "200%", None, Some(80)); assert!( io_only.ends_with("CPUQuota=200%\nIOWeight=80\n"), "{io_only}" diff --git a/nix/host-modules/hive-c0re/options.nix b/nix/host-modules/hive-c0re/options.nix index 8725e28e..eee0237c 100644 --- a/nix/host-modules/hive-c0re/options.nix +++ b/nix/host-modules/hive-c0re/options.nix @@ -302,9 +302,9 @@ }; agentCpuWeight = lib.mkOption { - type = lib.types.ints.between 1 10000; + type = lib.types.nullOr (lib.types.ints.between 1 10000); default = 80; - example = 50; + example = null; description = '' systemd `CPUWeight=` applied to every agent container via the same drop-in as `agentCpuQuota`. This is the cgroup v2 @@ -321,13 +321,17 @@ `hive-matrix`), which stay at `100`. Note this is a hive-wide value, so it does not rank agents against *each other*: they all share one weight. + + Set to `null` to leave `CPUWeight=` out of the drop-in entirely — + the container then inherits the kernel default and the generated + unit file is identical to one from before this option existed. ''; }; agentIoWeight = lib.mkOption { - type = lib.types.ints.between 1 10000; + type = lib.types.nullOr (lib.types.ints.between 1 10000); default = 80; - example = 50; + example = null; description = '' systemd `IOWeight=` applied to every agent container via the same drop-in as `agentCpuQuota` — the block-IO counterpart of @@ -340,7 +344,8 @@ scheduler. On a host running `none`/`mq-deadline`/`kyber` without iocost QoS configured, systemd writes the value and the kernel ignores it — harmless, but it will measure as a no-op. Check with - `cat /sys/fs/cgroup/io.cost.qos` on the host. + `cat /sys/fs/cgroup/io.cost.qos` on the host, and set this to + `null` to omit the setting rather than write one nothing reads. ''; };