swarm-wire: take agent_limits's wire types
This commit is contained in:
parent
266be0a5db
commit
9cce9c470f
11 changed files with 122 additions and 115 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
//! Reading the caps the swarm sets for this hive's agents.
|
//! Reading the caps the swarm sets for this hive's agents.
|
||||||
//!
|
//!
|
||||||
//! swarm-controller keeps one key per agent in the `agent-limits` bucket
|
//! swarm-controller keeps one key per agent in the `agent-limits` bucket
|
||||||
//! ([`swarm_queue_client::agent_limits`]). Every [`READ_INTERVAL`] this reads
|
//! ([`swarm_wire::agent_limits`]). Every [`READ_INTERVAL`] this reads
|
||||||
//! the key of each agent this hive has and stores what it finds in
|
//! the key of each agent this hive has and stores what it finds in
|
||||||
//! `swarm-resource-limits.json`, where the drop-in reads it.
|
//! `swarm-resource-limits.json`, where the drop-in reads it.
|
||||||
//! A running agent gets a changed cap at its first spawn or rebuild after
|
//! A running agent gets a changed cap at its first spawn or rebuild after
|
||||||
|
|
@ -14,7 +14,8 @@ use std::collections::BTreeMap;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use swarm_queue_client::agent_limits::{self, AgentCaps, Read};
|
use swarm_queue_client::agent_limits::{self, Read};
|
||||||
|
use swarm_wire::agent_limits::AgentCaps;
|
||||||
|
|
||||||
use crate::resource_limits::AgentLimits;
|
use crate::resource_limits::AgentLimits;
|
||||||
use crate::stats::sweep_health::{self, SweepHealth};
|
use crate::stats::sweep_health::{self, SweepHealth};
|
||||||
|
|
@ -127,7 +128,7 @@ mod tests {
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use super::validated;
|
use super::validated;
|
||||||
use swarm_queue_client::agent_limits::AgentCaps;
|
use swarm_wire::agent_limits::AgentCaps;
|
||||||
|
|
||||||
/// `atlas`'s answer with `atlas_key` as the value read for it, over the
|
/// `atlas`'s answer with `atlas_key` as the value read for it, over the
|
||||||
/// hive-wide defaults of 200% / 4G.
|
/// hive-wide defaults of 200% / 4G.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//! one agent.
|
//! one agent.
|
||||||
//!
|
//!
|
||||||
//! Each agent's caps are one value under its own key of `agent-limits`
|
//! Each agent's caps are one value under its own key of `agent-limits`
|
||||||
//! (`swarm_queue_client::agent_limits`), which this daemon alone writes. The
|
//! (`swarm_wire::agent_limits`), which this daemon alone writes. The
|
||||||
//! hive running the agent reads the key; on the hive, a cap replaces the
|
//! hive running the agent reads the key; on the hive, a cap replaces the
|
||||||
//! hive's own answer for that field, and a field left unset stays with the
|
//! hive's own answer for that field, and a field left unset stays with the
|
||||||
//! hive.
|
//! hive.
|
||||||
|
|
@ -16,7 +16,8 @@ use axum::extract::{Path, State};
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use problem_details::ProblemDetails;
|
use problem_details::ProblemDetails;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use swarm_queue_client::agent_limits::{self, AgentCaps};
|
use swarm_queue_client::agent_limits;
|
||||||
|
use swarm_wire::agent_limits::AgentCaps;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::routes::{AppState, error_problem, wanted_error_status};
|
use crate::routes::{AppState, error_problem, wanted_error_status};
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ pub(crate) struct ArchetypeView {
|
||||||
/// As `PUT /api/agents/{name}/limits` takes them.
|
/// As `PUT /api/agents/{name}/limits` takes them.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[schema(value_type = Option<Object>)]
|
#[schema(value_type = Option<Object>)]
|
||||||
limits: Option<swarm_queue_client::agent_limits::AgentCaps>,
|
limits: Option<swarm_wire::agent_limits::AgentCaps>,
|
||||||
/// As `PUT /api/agents/{name}/inference` takes them.
|
/// As `PUT /api/agents/{name}/inference` takes them.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[schema(value_type = Option<Object>)]
|
#[schema(value_type = Option<Object>)]
|
||||||
|
|
|
||||||
|
|
@ -566,7 +566,7 @@ impl Policy {
|
||||||
format!("$KV.{}.{hive}.*", swarm_wire::agent_status::BUCKET),
|
format!("$KV.{}.{hive}.*", swarm_wire::agent_status::BUCKET),
|
||||||
]);
|
]);
|
||||||
// Reading the caps the swarm sets for the agents this hive runs
|
// Reading the caps the swarm sets for the agents this hive runs
|
||||||
// (`swarm_queue_client::agent_limits`): a direct get, so read-only, and
|
// (`swarm_wire::agent_limits`): a direct get, so read-only, and
|
||||||
// no `STREAM.INFO` because the reader skips the lookup.
|
// no `STREAM.INFO` because the reader skips the lookup.
|
||||||
//
|
//
|
||||||
// Every agent's key, not just this hive's: the bucket is keyed by agent
|
// Every agent's key, not just this hive's: the bucket is keyed by agent
|
||||||
|
|
@ -574,7 +574,7 @@ impl Policy {
|
||||||
// `hive_name`'s doc argues against.
|
// `hive_name`'s doc argues against.
|
||||||
subjects.push(format!(
|
subjects.push(format!(
|
||||||
"$JS.API.DIRECT.GET.{}.>",
|
"$JS.API.DIRECT.GET.{}.>",
|
||||||
swarm_queue_client::agent_limits::STREAM
|
swarm_wire::agent_limits::STREAM
|
||||||
));
|
));
|
||||||
// The permissions the swarm grants those agents
|
// The permissions the swarm grants those agents
|
||||||
// (`swarm_queue_client::agent_permissions`), read the same way and
|
// (`swarm_queue_client::agent_permissions`), read the same way and
|
||||||
|
|
@ -847,7 +847,7 @@ mod tests {
|
||||||
/// grant reaches no other bucket.
|
/// grant reaches no other bucket.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_hive_may_read_every_agents_caps_and_write_none() {
|
fn a_hive_may_read_every_agents_caps_and_write_none() {
|
||||||
use swarm_queue_client::agent_limits::{STREAM, direct_get_subject, key_subject};
|
use swarm_wire::agent_limits::{STREAM, direct_get_subject, key_subject};
|
||||||
|
|
||||||
let p = policy()
|
let p = policy()
|
||||||
.permissions("hive-alpha")
|
.permissions("hive-alpha")
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@
|
||||||
//! **The hive running the agent reads the key** when it provisions or
|
//! **The hive running the agent reads the key** when it provisions or
|
||||||
//! rebuilds the agent, and renders the archetype's module into its meta flake
|
//! rebuilds the agent, and renders the archetype's module into its meta flake
|
||||||
//! in place of a config repo. Its grant covers every key of this bucket,
|
//! in place of a config repo. Its grant covers every key of this bucket,
|
||||||
//! read-only, for the reason [`crate::agent_limits`] gives.
|
//! read-only, for the reason `swarm_wire::agent_limits` gives.
|
||||||
//!
|
//!
|
||||||
//! Keyed by the agent alone, like [`crate::agent_limits`]: an agent can move
|
//! Keyed by the agent alone, like `swarm_wire::agent_limits`: an agent can move
|
||||||
//! between hives, and its archetype goes with it.
|
//! between hives, and its archetype goes with it.
|
||||||
|
|
||||||
/// The KV bucket archetype choices are published into, keyed by agent name.
|
/// The KV bucket archetype choices are published into, keyed by agent name.
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,6 @@
|
||||||
//! The per-agent resource-cap KV bucket: one key per agent, holding the CPU
|
//! The connection side of the per-agent resource-cap KV bucket: opening it for
|
||||||
//! and memory caps the swarm sets for it.
|
//! writing or reading, and the hive's direct read. The bucket's names, subjects
|
||||||
//!
|
//! and stored type are `swarm_wire::agent_limits`.
|
||||||
//! **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
|
|
||||||
//! ([`crate::agent_limits::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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open the caps bucket for writing, creating it if nothing has yet.
|
/// Open the caps bucket for writing, creating it if nothing has yet.
|
||||||
///
|
///
|
||||||
|
|
@ -64,7 +12,7 @@ pub async fn open_or_create(
|
||||||
let js = async_nats::jetstream::new(client.clone());
|
let js = async_nats::jetstream::new(client.clone());
|
||||||
crate::kv::open_or_create(
|
crate::kv::open_or_create(
|
||||||
&js,
|
&js,
|
||||||
BUCKET,
|
swarm_wire::agent_limits::BUCKET,
|
||||||
"agent-limits",
|
"agent-limits",
|
||||||
"Resource caps the swarm controller sets for each agent".to_owned(),
|
"Resource caps the swarm controller sets for each agent".to_owned(),
|
||||||
)
|
)
|
||||||
|
|
@ -85,11 +33,11 @@ pub async fn open(
|
||||||
client: &async_nats::Client,
|
client: &async_nats::Client,
|
||||||
) -> Result<Option<async_nats::jetstream::kv::Store>, crate::Error> {
|
) -> Result<Option<async_nats::jetstream::kv::Store>, crate::Error> {
|
||||||
let js = async_nats::jetstream::new(client.clone());
|
let js = async_nats::jetstream::new(client.clone());
|
||||||
match js.get_key_value(BUCKET).await {
|
match js.get_key_value(swarm_wire::agent_limits::BUCKET).await {
|
||||||
Ok(store) => Ok(Some(store)),
|
Ok(store) => Ok(Some(store)),
|
||||||
Err(e) if crate::wanted::is_bucket_not_found(&e) => Ok(None),
|
Err(e) if crate::wanted::is_bucket_not_found(&e) => Ok(None),
|
||||||
Err(source) => Err(crate::Error::OpenBucket {
|
Err(source) => Err(crate::Error::OpenBucket {
|
||||||
bucket: BUCKET.to_owned(),
|
bucket: swarm_wire::agent_limits::BUCKET.to_owned(),
|
||||||
source,
|
source,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +48,7 @@ pub async fn open(
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Read {
|
pub enum Read {
|
||||||
/// The bucket answered; an absent or deleted key has every field unset.
|
/// The bucket answered; an absent or deleted key has every field unset.
|
||||||
Current(AgentCaps),
|
Current(swarm_wire::agent_limits::AgentCaps),
|
||||||
/// Nothing answers for the bucket's stream: no cap has been written yet,
|
/// Nothing answers for the bucket's stream: no cap has been written yet,
|
||||||
/// or `JetStream` is not serving it right now. Not an unset value, since
|
/// or `JetStream` is not serving it right now. Not an unset value, since
|
||||||
/// the second case says nothing about the agent's caps.
|
/// the second case says nothing about the agent's caps.
|
||||||
|
|
@ -119,6 +67,7 @@ pub async fn read(
|
||||||
agent: &str,
|
agent: &str,
|
||||||
) -> Result<Read, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<Read, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
use async_nats::jetstream::stream::DirectGetErrorKind;
|
use async_nats::jetstream::stream::DirectGetErrorKind;
|
||||||
|
use swarm_wire::agent_limits::{AgentCaps, STREAM, key_subject};
|
||||||
|
|
||||||
let js = async_nats::jetstream::new(client.clone());
|
let js = async_nats::jetstream::new(client.clone());
|
||||||
let stream = js.get_stream_no_info(STREAM).await?;
|
let stream = js.get_stream_no_info(STREAM).await?;
|
||||||
|
|
@ -149,42 +98,3 @@ fn is_no_responders(e: &async_nats::jetstream::stream::DirectGetError) -> bool {
|
||||||
if status == async_nats::StatusCode::NO_RESPONDERS
|
if status == async_nats::StatusCode::NO_RESPONDERS
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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%"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@
|
||||||
//! **Two readers, each enforcing its own part.** The hive running the agent
|
//! **Two readers, each enforcing its own part.** The hive running the agent
|
||||||
//! applies `manage_root_agent` as bind mounts at container start; its grant
|
//! applies `manage_root_agent` as bind mounts at container start; its grant
|
||||||
//! covers every key of this bucket, read-only, for the reason
|
//! covers every key of this bucket, read-only, for the reason
|
||||||
//! [`crate::agent_limits`] gives. The agent itself (its harness and subagent
|
//! `swarm_wire::agent_limits` gives. The agent itself (its harness and subagent
|
||||||
//! daemon) applies the tool permissions; its grant covers only its own key.
|
//! daemon) applies the tool permissions; its grant covers only its own key.
|
||||||
//! Both read with one `DIRECT.GET` on the agent's key
|
//! Both read with one `DIRECT.GET` on the agent's key
|
||||||
//! ([`crate::agent_permissions::key_subject`]).
|
//! ([`crate::agent_permissions::key_subject`]).
|
||||||
//!
|
//!
|
||||||
//! Keyed by the agent alone, like [`crate::agent_limits`]: an agent can move
|
//! Keyed by the agent alone, like `swarm_wire::agent_limits`: an agent can move
|
||||||
//! between hives, and its permissions go with it.
|
//! between hives, and its permissions go with it.
|
||||||
//!
|
//!
|
||||||
//! Also carries the one-shot import a hive sends of the grants its meta repo
|
//! Also carries the one-shot import a hive sends of the grants its meta repo
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::agent_limits::AgentCaps;
|
use swarm_wire::agent_limits::AgentCaps;
|
||||||
use swarm_wire::agent_inference::AgentInference;
|
use swarm_wire::agent_inference::AgentInference;
|
||||||
|
|
||||||
/// The KV bucket archetype sources are published into, keyed by source name.
|
/// The KV bucket archetype sources are published into, keyed by source name.
|
||||||
|
|
|
||||||
|
|
@ -225,9 +225,10 @@ pub mod agent_schedules;
|
||||||
/// unconditional; writing needs `kv` and reading needs `agent-inference`.
|
/// unconditional; writing needs `kv` and reading needs `agent-inference`.
|
||||||
pub mod agent_inference;
|
pub mod agent_inference;
|
||||||
|
|
||||||
/// The bucket swarm-controller sets each agent's CPU and memory caps in — one
|
/// Opening and reading the bucket swarm-controller sets each agent's CPU and
|
||||||
/// key per agent, read by the hive that runs it. The names are unconditional;
|
/// memory caps in — one key per agent, read by the hive that runs it. The
|
||||||
/// writing needs `kv` and reading needs `agent-limits`.
|
/// names and the stored type are `swarm_wire::agent_limits`; writing needs `kv`
|
||||||
|
/// and reading needs `agent-limits`.
|
||||||
pub mod agent_limits;
|
pub mod agent_limits;
|
||||||
|
|
||||||
/// The bucket swarm-controller sets each agent's permissions in — one key
|
/// The bucket swarm-controller sets each agent's permissions in — one key
|
||||||
|
|
|
||||||
93
swarm-wire/src/agent_limits.rs
Normal file
93
swarm-wire/src/agent_limits.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
//! 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%"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
pub mod agent_icon;
|
pub mod agent_icon;
|
||||||
pub mod agent_inference;
|
pub mod agent_inference;
|
||||||
|
pub mod agent_limits;
|
||||||
pub mod agent_login;
|
pub mod agent_login;
|
||||||
pub mod agent_status;
|
pub mod agent_status;
|
||||||
pub mod builder;
|
pub mod builder;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue