use serde::{Deserialize, Serialize};
const GREEN_YELLOW_RATIO: f64 = 0.60;
const YELLOW_RED_RATIO: f64 = 0.80;
const FROM_SYSTEM_AVAIL_FRACTION: f64 = 0.50;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Pressure {
Green,
Yellow,
Red,
}
impl Pressure {
#[must_use]
pub fn is_red(self) -> bool {
matches!(self, Pressure::Red)
}
#[must_use]
pub fn is_yellow_or_red(self) -> bool {
!matches!(self, Pressure::Green)
}
}
#[derive(Debug, Clone)]
pub struct MemoryBudget {
pub max_rss_bytes: u64,
pub per_collection_soft_limit: u64,
pub flush_batch_size: usize,
}
impl MemoryBudget {
pub const DEFAULT_SOFT_LIMIT: u64 = 256 * 1024 * 1024;
pub const DEFAULT_FLUSH_BATCH: usize = 5_000;
#[must_use]
pub fn from_system() -> Self {
let avail = Self::probe_available_memory();
let max_rss = if avail > 0 {
((avail as f64) * FROM_SYSTEM_AVAIL_FRACTION) as u64
} else {
1024 * 1024 * 1024 };
Self {
max_rss_bytes: max_rss,
per_collection_soft_limit: Self::DEFAULT_SOFT_LIMIT,
flush_batch_size: Self::DEFAULT_FLUSH_BATCH,
}
}
#[must_use]
pub fn new(max_rss_bytes: u64) -> Self {
Self {
max_rss_bytes,
per_collection_soft_limit: Self::DEFAULT_SOFT_LIMIT,
flush_batch_size: Self::DEFAULT_FLUSH_BATCH,
}
}
#[must_use]
pub fn with_soft_limit(mut self, bytes: u64) -> Self {
self.per_collection_soft_limit = bytes;
self
}
#[must_use]
pub fn with_flush_batch_size(mut self, size: usize) -> Self {
self.flush_batch_size = size;
self
}
#[must_use]
pub fn check_pressure(&self, current_rss: u64) -> Pressure {
if self.max_rss_bytes == 0 {
return Pressure::Red;
}
let ratio = (current_rss as f64) / (self.max_rss_bytes as f64);
if ratio >= YELLOW_RED_RATIO {
Pressure::Red
} else if ratio >= GREEN_YELLOW_RATIO {
Pressure::Yellow
} else {
Pressure::Green
}
}
#[must_use]
pub fn collection_exceeds_limit(&self, size: u64) -> bool {
size >= self.per_collection_soft_limit
}
#[must_use]
pub fn probe_available_memory() -> u64 {
let mut sys = sysinfo::System::new();
sys.refresh_memory();
sys.available_memory()
}
}
impl Default for MemoryBudget {
fn default() -> Self {
Self::from_system()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pressure_is_red_returns_true_only_for_red() {
assert!(!Pressure::Green.is_red());
assert!(!Pressure::Yellow.is_red());
assert!(Pressure::Red.is_red());
}
#[test]
fn pressure_is_yellow_or_red_returns_true_for_yellow_and_red() {
assert!(!Pressure::Green.is_yellow_or_red());
assert!(Pressure::Yellow.is_yellow_or_red());
assert!(Pressure::Red.is_yellow_or_red());
}
#[test]
fn pressure_is_send_sync() {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<Pressure>();
}
#[test]
fn pressure_serde_roundtrip() {
for p in [Pressure::Green, Pressure::Yellow, Pressure::Red] {
let json = serde_json::to_string(&p).expect("serialize");
let back: Pressure = serde_json::from_str(&json).expect("deserialize");
assert_eq!(p, back, "roundtrip failed for {p:?}");
}
}
#[test]
fn default_soft_limit_is_256mib() {
assert_eq!(MemoryBudget::DEFAULT_SOFT_LIMIT, 256 * 1024 * 1024);
}
#[test]
fn default_flush_batch_is_5000() {
assert_eq!(MemoryBudget::DEFAULT_FLUSH_BATCH, 5_000);
}
#[test]
fn new_sets_max_rss_and_defaults() {
let b = MemoryBudget::new(2 * 1024 * 1024 * 1024); assert_eq!(b.max_rss_bytes, 2 * 1024 * 1024 * 1024);
assert_eq!(
b.per_collection_soft_limit,
MemoryBudget::DEFAULT_SOFT_LIMIT
);
assert_eq!(b.flush_batch_size, MemoryBudget::DEFAULT_FLUSH_BATCH);
}
#[test]
fn new_with_zero_max_rss_is_allowed_but_always_red() {
let b = MemoryBudget::new(0);
assert_eq!(b.check_pressure(0), Pressure::Red);
assert_eq!(b.check_pressure(1), Pressure::Red);
}
#[test]
fn with_soft_limit_overrides_default() {
let b = MemoryBudget::new(1024).with_soft_limit(512 * 1024 * 1024);
assert_eq!(b.per_collection_soft_limit, 512 * 1024 * 1024);
}
#[test]
fn with_flush_batch_size_overrides_default() {
let b = MemoryBudget::new(1024).with_flush_batch_size(1_000);
assert_eq!(b.flush_batch_size, 1_000);
}
#[test]
fn builder_methods_chain() {
let b = MemoryBudget::new(1024)
.with_soft_limit(100)
.with_flush_batch_size(200);
assert_eq!(b.per_collection_soft_limit, 100);
assert_eq!(b.flush_batch_size, 200);
}
#[test]
fn check_pressure_zero_rss_is_green() {
let b = MemoryBudget::new(1_000_000);
assert_eq!(b.check_pressure(0), Pressure::Green);
}
#[test]
fn check_pressure_just_below_yellow_is_green() {
let b = MemoryBudget::new(1_000_000);
assert_eq!(b.check_pressure(599_999), Pressure::Green);
}
#[test]
fn check_pressure_at_60_percent_is_yellow() {
let b = MemoryBudget::new(1_000_000);
assert_eq!(b.check_pressure(600_000), Pressure::Yellow);
}
#[test]
fn check_pressure_just_below_red_is_yellow() {
let b = MemoryBudget::new(1_000_000);
assert_eq!(b.check_pressure(799_999), Pressure::Yellow);
}
#[test]
fn check_pressure_at_80_percent_is_red() {
let b = MemoryBudget::new(1_000_000);
assert_eq!(b.check_pressure(800_000), Pressure::Red);
}
#[test]
fn check_pressure_at_full_cap_is_red() {
let b = MemoryBudget::new(1_000_000);
assert_eq!(b.check_pressure(1_000_000), Pressure::Red);
}
#[test]
fn check_pressure_above_cap_is_red() {
let b = MemoryBudget::new(1_000_000);
assert_eq!(b.check_pressure(2_000_000), Pressure::Red);
}
#[test]
fn collection_exceeds_limit_below_limit_returns_false() {
let b = MemoryBudget::new(1024).with_soft_limit(1_000);
assert!(!b.collection_exceeds_limit(999));
}
#[test]
fn collection_exceeds_limit_at_limit_returns_true() {
let b = MemoryBudget::new(1024).with_soft_limit(1_000);
assert!(b.collection_exceeds_limit(1_000));
}
#[test]
fn collection_exceeds_limit_above_limit_returns_true() {
let b = MemoryBudget::new(1024).with_soft_limit(1_000);
assert!(b.collection_exceeds_limit(1_001));
}
#[test]
fn from_system_returns_nonzero_max_rss() {
let b = MemoryBudget::from_system();
assert!(b.max_rss_bytes > 0, "from_system returned zero max_rss");
}
#[test]
fn from_system_applies_other_defaults() {
let b = MemoryBudget::from_system();
assert_eq!(
b.per_collection_soft_limit,
MemoryBudget::DEFAULT_SOFT_LIMIT
);
assert_eq!(b.flush_batch_size, MemoryBudget::DEFAULT_FLUSH_BATCH);
}
#[test]
fn default_equals_from_system_invariants() {
let d = MemoryBudget::default();
assert!(d.max_rss_bytes > 0);
assert_eq!(
d.per_collection_soft_limit,
MemoryBudget::DEFAULT_SOFT_LIMIT
);
}
#[test]
fn probe_available_memory_returns_some_value_on_normal_host() {
let avail = MemoryBudget::probe_available_memory();
assert!(avail > 0, "probe_available_memory returned 0 — sandbox?");
}
#[test]
fn memory_budget_is_send_sync() {
fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<MemoryBudget>();
}
#[test]
fn memory_budget_is_clone() {
let b = MemoryBudget::new(1024);
let _b2 = b.clone();
}
#[test]
fn very_large_max_rss_does_not_overflow() {
let b = MemoryBudget::new(u64::MAX);
assert_eq!(b.check_pressure(0), Pressure::Green);
assert_eq!(b.check_pressure(u64::MAX), Pressure::Red);
}
#[test]
fn check_pressure_with_one_byte_max_rss_classifies_correctly() {
let b = MemoryBudget::new(1);
assert_eq!(b.check_pressure(0), Pressure::Green);
assert_eq!(b.check_pressure(1), Pressure::Red);
}
}