Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-wire/src/agent_limits.rs

93 lines
3.3 KiB
Rust

//! The per-agent resource-cap KV bucket: one key per agent, holding the CPU
//! and memory caps the swarm sets for it.
//!
//! **swarm-controller is the only writer**, and creates the bucket on its
//! first write. Every field is optional: an absent field leaves the agent on
//! its hive's own answer, so an agent with no key runs exactly as it would
//! with no swarm.
//!
//! **The hive running the agent reads the key**, not the agent: the caps go
//! into the container's systemd drop-in, which the host writes. A hive reads
//! with one `DIRECT.GET` per agent ([`direct_get_subject`]), and its grant
//! covers every key of this bucket, read-only. Scoping it to the hive's own
//! agents would need a roster in the auth responder.
//!
//! Keyed by the agent alone, like [`crate::agent_inference`]: an agent can
//! move between hives, and its caps go with it.
/// The KV bucket caps are published into, keyed by agent name.
pub const BUCKET: &str = "agent-limits";
/// The stream backing [`BUCKET`]. `create_key_value` prefixes `KV_`.
pub const STREAM: &str = "KV_agent-limits";
/// The subject `agent`'s entry is stored under.
#[must_use]
pub fn key_subject(agent: &str) -> String {
format!("$KV.{BUCKET}.{agent}")
}
/// The request subject that reads `agent`'s current entry.
#[must_use]
pub fn direct_get_subject(agent: &str) -> String {
format!("$JS.API.DIRECT.GET.{STREAM}.{}", key_subject(agent))
}
/// One agent's caps: the value under `BUCKET`/`<agent>`, as systemd values
/// carried verbatim. `None` is "not capped by the swarm", never "uncapped".
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AgentCaps {
/// systemd `CPUQuota=`, e.g. `"400%"`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_quota: Option<String>,
/// systemd `MemoryMax=`, e.g. `"8G"`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory_max: Option<String>,
}
impl AgentCaps {
/// True when no field is set, which a hive reads the same as no key.
#[must_use]
pub fn is_empty(&self) -> bool {
self.cpu_quota.is_none() && self.memory_max.is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The names the hive's grant in `swarm-nats-auth` is built from.
#[test]
fn names_match_the_queue_grant() {
assert_eq!(STREAM, format!("KV_{BUCKET}"));
assert_eq!(key_subject("iris"), "$KV.agent-limits.iris");
assert_eq!(
direct_get_subject("iris"),
"$JS.API.DIRECT.GET.KV_agent-limits.$KV.agent-limits.iris"
);
}
/// An unset field is left out on the wire, and `{}` is every field unset.
#[test]
fn unset_fields_are_absent() {
let value = AgentCaps {
memory_max: Some("8G".to_owned()),
..AgentCaps::default()
};
assert_eq!(
serde_json::to_string(&value).unwrap(),
r#"{"memory_max":"8G"}"#
);
let empty: AgentCaps = serde_json::from_str("{}").unwrap();
assert!(empty.is_empty());
}
/// Fields a newer controller adds must not cost an older hive its caps.
#[test]
fn decoding_tolerates_unknown_fields() {
let decoded: AgentCaps =
serde_json::from_str(r#"{"cpu_quota":"400%","io_weight":"10"}"#).unwrap();
assert_eq!(decoded.cpu_quota.as_deref(), Some("400%"));
}
}