#[cfg(feature = "report-serde")]
use serde::ser::{Serialize, SerializeStruct as _, Serializer};
#[derive(Debug, Clone, Copy, Default, PartialEq)]
#[non_exhaustive]
pub struct ResourceLimits {
pub max_memory: Option<u64>,
pub max_processes: Option<u32>,
pub cpu_quota: Option<f64>,
}
impl ResourceLimits {
pub(crate) fn any(&self) -> bool {
self.max_memory.is_some() || self.max_processes.is_some() || self.cpu_quota.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LimitKind {
Memory,
Processes,
Cpu,
}
impl LimitKind {
pub fn name(&self) -> &'static str {
match self {
LimitKind::Memory => "memory",
LimitKind::Processes => "processes",
LimitKind::Cpu => "cpu",
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
"memory" => Some(LimitKind::Memory),
"processes" => Some(LimitKind::Processes),
"cpu" => Some(LimitKind::Cpu),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LimitReason {
Invalid,
Unsupported,
Unenforceable,
}
impl LimitReason {
pub fn name(&self) -> &'static str {
match self {
LimitReason::Invalid => "invalid",
LimitReason::Unsupported => "unsupported",
LimitReason::Unenforceable => "unenforceable",
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
"invalid" => Some(LimitReason::Invalid),
"unsupported" => Some(LimitReason::Unsupported),
"unenforceable" => Some(LimitReason::Unenforceable),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LimitVerdict {
Tripped,
NotTripped,
Unknown,
}
impl LimitVerdict {
pub fn name(&self) -> &'static str {
match self {
LimitVerdict::Tripped => "tripped",
LimitVerdict::NotTripped => "not_tripped",
LimitVerdict::Unknown => "unknown",
}
}
pub fn from_name(name: &str) -> Option<Self> {
match name {
"tripped" => Some(LimitVerdict::Tripped),
"not_tripped" => Some(LimitVerdict::NotTripped),
"unknown" => Some(LimitVerdict::Unknown),
_ => None,
}
}
}
#[cfg(feature = "report-serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "report-serde")))]
impl Serialize for LimitVerdict {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct LimitEvidence {
memory: LimitVerdict,
processes: LimitVerdict,
cpu: LimitVerdict,
}
impl LimitEvidence {
#[cfg_attr(all(unix, not(target_os = "linux")), allow(dead_code))]
pub(crate) const fn new(
memory: LimitVerdict,
processes: LimitVerdict,
cpu: LimitVerdict,
) -> Self {
Self {
memory,
processes,
cpu,
}
}
#[cfg_attr(windows, allow(dead_code))]
pub(crate) const fn unknown() -> Self {
Self {
memory: LimitVerdict::Unknown,
processes: LimitVerdict::Unknown,
cpu: LimitVerdict::Unknown,
}
}
pub fn memory(&self) -> LimitVerdict {
self.memory
}
pub fn processes(&self) -> LimitVerdict {
self.processes
}
pub fn cpu(&self) -> LimitVerdict {
self.cpu
}
pub fn verdict(&self, kind: LimitKind) -> LimitVerdict {
match kind {
LimitKind::Memory => self.memory,
LimitKind::Processes => self.processes,
LimitKind::Cpu => self.cpu,
}
}
}
#[cfg(feature = "report-serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "report-serde")))]
impl Serialize for LimitEvidence {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let Self {
memory,
processes,
cpu,
} = self;
let mut state = serializer.serialize_struct("LimitEvidence", 3)?;
state.serialize_field("memory", memory)?;
state.serialize_field("processes", processes)?;
state.serialize_field("cpu", cpu)?;
state.end()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct CappedAxes {
memory: bool,
processes: bool,
cpu: bool,
}
impl CappedAxes {
pub(crate) fn record(&mut self, limits: &ResourceLimits) {
self.memory |= limits.max_memory.is_some();
self.processes |= limits.max_processes.is_some();
self.cpu |= limits.cpu_quota.is_some();
}
#[cfg_attr(all(unix, not(target_os = "linux")), allow(dead_code))]
pub(crate) fn has(&self, kind: LimitKind) -> bool {
match kind {
LimitKind::Memory => self.memory,
LimitKind::Processes => self.processes,
LimitKind::Cpu => self.cpu,
}
}
}
#[cfg(test)]
mod tests {
use super::{CappedAxes, LimitEvidence, LimitKind, LimitReason, LimitVerdict, ResourceLimits};
const ALL_KINDS: &[LimitKind] = &[LimitKind::Memory, LimitKind::Processes, LimitKind::Cpu];
const ALL_REASONS: &[LimitReason] = &[
LimitReason::Invalid,
LimitReason::Unsupported,
LimitReason::Unenforceable,
];
#[test]
fn limit_kind_name_pins_each_variant() {
assert_eq!(LimitKind::Memory.name(), "memory");
assert_eq!(LimitKind::Processes.name(), "processes");
assert_eq!(LimitKind::Cpu.name(), "cpu");
}
#[test]
fn limit_reason_name_pins_each_variant() {
assert_eq!(LimitReason::Invalid.name(), "invalid");
assert_eq!(LimitReason::Unsupported.name(), "unsupported");
assert_eq!(LimitReason::Unenforceable.name(), "unenforceable");
}
#[test]
fn name_from_name_round_trips_every_variant() {
for &k in ALL_KINDS {
assert_eq!(LimitKind::from_name(k.name()), Some(k));
}
for &r in ALL_REASONS {
assert_eq!(LimitReason::from_name(r.name()), Some(r));
}
}
#[test]
fn from_name_rejects_unknown_without_defaulting() {
assert_eq!(LimitKind::from_name("Memory"), None);
assert_eq!(LimitKind::from_name("ram"), None);
assert_eq!(LimitReason::from_name(""), None);
assert_eq!(LimitReason::from_name("unenforced"), None);
}
#[test]
fn limit_verdict_name_pins_each_variant_and_round_trips() {
assert_eq!(LimitVerdict::Tripped.name(), "tripped");
assert_eq!(LimitVerdict::NotTripped.name(), "not_tripped");
assert_eq!(LimitVerdict::Unknown.name(), "unknown");
for v in [
LimitVerdict::Tripped,
LimitVerdict::NotTripped,
LimitVerdict::Unknown,
] {
assert_eq!(LimitVerdict::from_name(v.name()), Some(v));
}
assert_eq!(LimitVerdict::from_name("Tripped"), None);
assert_eq!(LimitVerdict::from_name("nottripped"), None);
assert_eq!(LimitVerdict::from_name(""), None);
}
#[test]
fn limit_evidence_addresses_each_axis_by_kind_and_by_name() {
let ev = LimitEvidence::new(
LimitVerdict::Tripped,
LimitVerdict::NotTripped,
LimitVerdict::Unknown,
);
assert_eq!(ev.memory(), LimitVerdict::Tripped);
assert_eq!(ev.processes(), LimitVerdict::NotTripped);
assert_eq!(ev.cpu(), LimitVerdict::Unknown);
for &k in ALL_KINDS {
let named = match k {
LimitKind::Memory => ev.memory(),
LimitKind::Processes => ev.processes(),
LimitKind::Cpu => ev.cpu(),
};
assert_eq!(ev.verdict(k), named, "axis {k:?}");
}
}
#[test]
fn unknown_evidence_is_unknown_on_every_axis() {
let ev = LimitEvidence::unknown();
for &k in ALL_KINDS {
assert_eq!(ev.verdict(k), LimitVerdict::Unknown, "axis {k:?}");
}
}
#[test]
fn capped_axes_are_sticky_across_a_lifted_cap() {
let mut axes = CappedAxes::default();
for &k in ALL_KINDS {
assert!(!axes.has(k));
}
axes.record(&ResourceLimits {
max_memory: Some(64 * 1024 * 1024),
..ResourceLimits::default()
});
assert!(axes.has(LimitKind::Memory));
assert!(!axes.has(LimitKind::Processes));
assert!(!axes.has(LimitKind::Cpu));
axes.record(&ResourceLimits {
max_processes: Some(4),
..ResourceLimits::default()
});
assert!(axes.has(LimitKind::Memory), "a lifted cap stays recorded");
assert!(axes.has(LimitKind::Processes));
assert!(
!axes.has(LimitKind::Cpu),
"an untouched axis stays unrecorded"
);
axes.record(&ResourceLimits {
cpu_quota: Some(0.5),
..ResourceLimits::default()
});
for &k in ALL_KINDS {
assert!(axes.has(k));
}
}
#[test]
fn any_detects_each_limit_axis_independently() {
assert!(!ResourceLimits::default().any());
assert!(
ResourceLimits {
max_memory: Some(1),
..ResourceLimits::default()
}
.any()
);
assert!(
ResourceLimits {
max_processes: Some(1),
..ResourceLimits::default()
}
.any()
);
assert!(
ResourceLimits {
cpu_quota: Some(0.5),
..ResourceLimits::default()
}
.any()
);
}
}