use std::collections::BTreeMap;
use mongreldb_types::ids::TabletId;
use serde::{Deserialize, Serialize};
use crate::memory::{EscalationLevel, GovernorStats, MemoryGovernor};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct NodePressureInputs {
pub physical_memory_bytes: u64,
pub configured_max_bytes: u64,
pub os_pressure: f64,
pub cache_hit_rate: f64,
pub query_reserved_bytes: u64,
pub compaction_backlog_bytes: u64,
pub replication_backlog_bytes: u64,
}
impl Default for NodePressureInputs {
fn default() -> Self {
Self {
physical_memory_bytes: 16 * 1024 * 1024 * 1024,
configured_max_bytes: 12 * 1024 * 1024 * 1024,
os_pressure: 0.0,
cache_hit_rate: 1.0,
query_reserved_bytes: 0,
compaction_backlog_bytes: 0,
replication_backlog_bytes: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GovernorAction {
EvictCaches,
ReduceAdmission,
SpillAnalytics,
ThrottleCompaction,
MoveTabletLeaders {
tablets: Vec<TabletId>,
},
RejectOversizedAi,
}
#[derive(Debug)]
pub struct NodeMemoryGovernor {
pub governor: MemoryGovernor,
tablet_reserved: BTreeMap<TabletId, u64>,
last_actions: Vec<GovernorAction>,
}
impl NodeMemoryGovernor {
pub fn new(governor: MemoryGovernor) -> Self {
Self {
governor,
tablet_reserved: BTreeMap::new(),
last_actions: Vec::new(),
}
}
pub fn adjust_tablet(&mut self, tablet: TabletId, delta: i64) {
let entry = self.tablet_reserved.entry(tablet).or_insert(0);
if delta >= 0 {
*entry = entry.saturating_add(delta as u64);
} else {
*entry = entry.saturating_sub((-delta) as u64);
}
if *entry == 0 {
self.tablet_reserved.remove(&tablet);
}
}
pub fn evaluate(&mut self, inputs: &NodePressureInputs) -> Vec<GovernorAction> {
let stats: GovernorStats = self.governor.stats();
let used = stats.total_used.saturating_add(inputs.query_reserved_bytes);
let max = inputs.configured_max_bytes.max(1);
let pressure = (used as f64 / max as f64).max(inputs.os_pressure);
let mut actions = Vec::new();
if pressure >= 0.70 || inputs.os_pressure >= 0.70 {
actions.push(GovernorAction::EvictCaches);
}
if pressure >= 0.80 {
actions.push(GovernorAction::ReduceAdmission);
actions.push(GovernorAction::SpillAnalytics);
}
if pressure >= 0.85 || inputs.compaction_backlog_bytes > max / 8 {
actions.push(GovernorAction::ThrottleCompaction);
}
if pressure >= 0.90 {
let tablets: Vec<TabletId> = self.tablet_reserved.keys().copied().collect();
actions.push(GovernorAction::MoveTabletLeaders { tablets });
actions.push(GovernorAction::RejectOversizedAi);
}
match self.governor.escalation() {
EscalationLevel::None => {}
EscalationLevel::RejectLowPriority => {
if !actions.contains(&GovernorAction::ReduceAdmission) {
actions.push(GovernorAction::ReduceAdmission);
}
}
EscalationLevel::EvictCaches | EscalationLevel::SpillOperators => {
if !actions.contains(&GovernorAction::EvictCaches) {
actions.push(GovernorAction::EvictCaches);
}
if !actions.contains(&GovernorAction::SpillAnalytics) {
actions.push(GovernorAction::SpillAnalytics);
}
}
EscalationLevel::ThrottleMaintenance => {
if !actions
.iter()
.any(|a| matches!(a, GovernorAction::MoveTabletLeaders { .. }))
{
actions.push(GovernorAction::MoveTabletLeaders {
tablets: self.tablet_reserved.keys().copied().collect(),
});
}
}
}
self.last_actions = actions.clone();
actions
}
pub fn last_actions(&self) -> &[GovernorAction] {
&self.last_actions
}
pub fn tablet_reserved_bytes(&self) -> u64 {
self.tablet_reserved.values().sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::{GovernorConfig, MemoryGovernor};
fn gov(max: u64) -> NodeMemoryGovernor {
NodeMemoryGovernor::new(MemoryGovernor::new(GovernorConfig::new(max)).unwrap())
}
#[test]
fn high_pressure_emits_oom_prevention_actions() {
let mut g = gov(1_000_000);
let mut inputs = NodePressureInputs {
configured_max_bytes: 1_000_000,
query_reserved_bytes: 950_000,
os_pressure: 0.95,
..NodePressureInputs::default()
};
let actions = g.evaluate(&inputs);
assert!(actions
.iter()
.any(|a| matches!(a, GovernorAction::EvictCaches)));
assert!(actions
.iter()
.any(|a| matches!(a, GovernorAction::RejectOversizedAi)));
assert!(actions
.iter()
.any(|a| matches!(a, GovernorAction::MoveTabletLeaders { .. })));
inputs.os_pressure = 0.5;
inputs.query_reserved_bytes = 1000;
let calm = g.evaluate(&inputs);
assert!(calm.len() < actions.len());
}
#[test]
fn tablet_accounting() {
let mut g = gov(10_000_000);
let t = TabletId::from_bytes([1; 16]);
g.adjust_tablet(t, 1000);
assert_eq!(g.tablet_reserved_bytes(), 1000);
g.adjust_tablet(t, -1000);
assert_eq!(g.tablet_reserved_bytes(), 0);
}
}