1use std::fmt;
39use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
40use std::sync::{Arc, Weak};
41
42use serde::{Deserialize, Serialize};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
50pub enum MemoryClass {
51 PageCache,
53 DecodedCache,
55 QueryExecution,
57 ResultBuffering,
59 AiCandidates,
61 Compaction,
63 Replication,
65 Backup,
67 NetworkBuffers,
70}
71
72impl MemoryClass {
73 pub const COUNT: usize = 9;
75
76 pub const ALL: [MemoryClass; MemoryClass::COUNT] = [
78 MemoryClass::PageCache,
79 MemoryClass::DecodedCache,
80 MemoryClass::QueryExecution,
81 MemoryClass::ResultBuffering,
82 MemoryClass::AiCandidates,
83 MemoryClass::Compaction,
84 MemoryClass::Replication,
85 MemoryClass::Backup,
86 MemoryClass::NetworkBuffers,
87 ];
88
89 pub fn index(self) -> usize {
91 Self::ALL
92 .iter()
93 .position(|c| *c == self)
94 .expect("ALL is total")
95 }
96
97 pub fn name(self) -> &'static str {
99 match self {
100 MemoryClass::PageCache => "page_cache",
101 MemoryClass::DecodedCache => "decoded_cache",
102 MemoryClass::QueryExecution => "query_execution",
103 MemoryClass::ResultBuffering => "result_buffering",
104 MemoryClass::AiCandidates => "ai_candidates",
105 MemoryClass::Compaction => "compaction",
106 MemoryClass::Replication => "replication",
107 MemoryClass::Backup => "backup",
108 MemoryClass::NetworkBuffers => "network_buffers",
109 }
110 }
111
112 pub fn is_reserved(self) -> bool {
117 matches!(self, MemoryClass::Replication | MemoryClass::NetworkBuffers)
118 }
119
120 pub fn is_low_priority(self) -> bool {
123 matches!(self, MemoryClass::Compaction | MemoryClass::Backup)
124 }
125
126 pub fn is_reclaimable_cache(self) -> bool {
129 matches!(self, MemoryClass::PageCache | MemoryClass::DecodedCache)
130 }
131
132 pub fn is_spill_eligible(self) -> bool {
138 matches!(
139 self,
140 MemoryClass::QueryExecution | MemoryClass::ResultBuffering
141 )
142 }
143}
144
145impl fmt::Display for MemoryClass {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 f.write_str(self.name())
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
153pub enum MemoryError {
154 #[error("invalid memory governor configuration: {0}")]
156 InvalidConfig(&'static str),
157 #[error(
160 "memory reservation of {requested} bytes for {class} rejected: {available} bytes available"
161 )]
162 Exhausted {
163 class: MemoryClass,
165 requested: u64,
167 available: u64,
169 },
170 #[error("low-priority memory reservation for {class} rejected: memory pressure (S1E-003 escalation step 1)")]
172 LowPriorityRejected {
173 class: MemoryClass,
175 },
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
184pub struct EscalationThresholds {
185 pub reject_low_priority: f64,
187 pub evict_caches: f64,
189 pub spill_operators: f64,
191 pub throttle_maintenance: f64,
193 pub hysteresis: f64,
196}
197
198impl Default for EscalationThresholds {
199 fn default() -> Self {
200 Self {
201 reject_low_priority: 0.70,
202 evict_caches: 0.80,
203 spill_operators: 0.90,
204 throttle_maintenance: 0.95,
205 hysteresis: 0.05,
206 }
207 }
208}
209
210impl EscalationThresholds {
211 fn validate(&self) -> Result<(), MemoryError> {
212 let in_band = |v: f64| v > 0.0 && v <= 1.0;
213 if !in_band(self.reject_low_priority)
214 || !in_band(self.evict_caches)
215 || !in_band(self.spill_operators)
216 || !in_band(self.throttle_maintenance)
217 {
218 return Err(MemoryError::InvalidConfig(
219 "escalation thresholds must be in (0, 1]",
220 ));
221 }
222 if !(self.reject_low_priority < self.evict_caches
223 && self.evict_caches < self.spill_operators
224 && self.spill_operators < self.throttle_maintenance)
225 {
226 return Err(MemoryError::InvalidConfig(
227 "escalation thresholds must be strictly increasing in S1E-003 order",
228 ));
229 }
230 if !(0.0..self.reject_low_priority).contains(&self.hysteresis) {
231 return Err(MemoryError::InvalidConfig(
232 "hysteresis must be in [0, reject_low_priority)",
233 ));
234 }
235 Ok(())
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241pub struct GovernorConfig {
242 pub max_bytes: u64,
244 pub reserved_floor_bytes: u64,
247 pub class_budgets: [u64; MemoryClass::COUNT],
250 pub thresholds: EscalationThresholds,
252}
253
254impl GovernorConfig {
255 pub fn new(max_bytes: u64) -> Self {
258 Self {
259 max_bytes,
260 reserved_floor_bytes: max_bytes / 8,
261 class_budgets: [max_bytes; MemoryClass::COUNT],
262 thresholds: EscalationThresholds::default(),
263 }
264 }
265
266 pub fn with_class_budget(mut self, class: MemoryClass, bytes: u64) -> Self {
268 self.class_budgets[class.index()] = bytes;
269 self
270 }
271
272 pub fn with_reserved_floor(mut self, bytes: u64) -> Self {
274 self.reserved_floor_bytes = bytes;
275 self
276 }
277
278 pub fn with_thresholds(mut self, thresholds: EscalationThresholds) -> Self {
280 self.thresholds = thresholds;
281 self
282 }
283
284 pub fn validate(&self) -> Result<(), MemoryError> {
286 if self.max_bytes == 0 {
287 return Err(MemoryError::InvalidConfig("max_bytes must be nonzero"));
288 }
289 if self.reserved_floor_bytes > self.max_bytes {
290 return Err(MemoryError::InvalidConfig(
291 "reserved_floor_bytes must not exceed max_bytes",
292 ));
293 }
294 if self.class_budgets.iter().any(|b| *b > self.max_bytes) {
295 return Err(MemoryError::InvalidConfig(
296 "class budgets must not exceed max_bytes",
297 ));
298 }
299 self.thresholds.validate()
300 }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
307pub enum EscalationLevel {
308 None = 0,
310 RejectLowPriority = 1,
312 EvictCaches = 2,
314 SpillOperators = 3,
316 ThrottleMaintenance = 4,
318}
319
320impl EscalationLevel {
321 fn from_u8(v: u8) -> Self {
322 match v {
323 0 => EscalationLevel::None,
324 1 => EscalationLevel::RejectLowPriority,
325 2 => EscalationLevel::EvictCaches,
326 3 => EscalationLevel::SpillOperators,
327 _ => EscalationLevel::ThrottleMaintenance,
328 }
329 }
330
331 fn up(self) -> Option<Self> {
333 match self {
334 EscalationLevel::None => Some(EscalationLevel::RejectLowPriority),
335 EscalationLevel::RejectLowPriority => Some(EscalationLevel::EvictCaches),
336 EscalationLevel::EvictCaches => Some(EscalationLevel::SpillOperators),
337 EscalationLevel::SpillOperators => Some(EscalationLevel::ThrottleMaintenance),
338 EscalationLevel::ThrottleMaintenance => None,
339 }
340 }
341
342 fn down(self) -> Self {
344 match self {
345 EscalationLevel::None => EscalationLevel::None,
346 EscalationLevel::RejectLowPriority => EscalationLevel::None,
347 EscalationLevel::EvictCaches => EscalationLevel::RejectLowPriority,
348 EscalationLevel::SpillOperators => EscalationLevel::EvictCaches,
349 EscalationLevel::ThrottleMaintenance => EscalationLevel::SpillOperators,
350 }
351 }
352
353 fn threshold(self, t: &EscalationThresholds) -> f64 {
355 match self {
356 EscalationLevel::None => 0.0,
357 EscalationLevel::RejectLowPriority => t.reject_low_priority,
358 EscalationLevel::EvictCaches => t.evict_caches,
359 EscalationLevel::SpillOperators => t.spill_operators,
360 EscalationLevel::ThrottleMaintenance => t.throttle_maintenance,
361 }
362 }
363
364 pub fn name(self) -> &'static str {
366 match self {
367 EscalationLevel::None => "none",
368 EscalationLevel::RejectLowPriority => "reject_low_priority",
369 EscalationLevel::EvictCaches => "evict_caches",
370 EscalationLevel::SpillOperators => "spill_operators",
371 EscalationLevel::ThrottleMaintenance => "throttle_maintenance",
372 }
373 }
374}
375
376impl fmt::Display for EscalationLevel {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 f.write_str(self.name())
379 }
380}
381
382pub trait Reclaimable: Send + Sync {
386 fn evict_reclaimable(&self, budget: u64) -> u64;
389 fn reclaimable_bytes(&self) -> u64;
391}
392
393struct Inner {
394 config: GovernorConfig,
395 class_used: [AtomicU64; MemoryClass::COUNT],
396 total_used: AtomicU64,
397 escalation: AtomicU8,
398 reservations_granted: AtomicU64,
399 reservations_rejected: AtomicU64,
400 low_priority_rejected: AtomicU64,
401 spill_triggers: AtomicU64,
402 reclaimers: parking_lot::Mutex<Vec<Weak<dyn Reclaimable>>>,
405}
406
407#[derive(Debug, Clone)]
409pub struct GovernorStats {
410 pub max_bytes: u64,
412 pub reserved_floor_bytes: u64,
414 pub total_used: u64,
416 pub class_used: [u64; MemoryClass::COUNT],
418 pub pressure: f64,
420 pub escalation: EscalationLevel,
422 pub reservations_granted: u64,
424 pub reservations_rejected: u64,
426 pub low_priority_rejected: u64,
428 pub spill_triggers: u64,
430}
431
432impl GovernorStats {
433 pub fn usage_for(&self, class: MemoryClass) -> u64 {
435 self.class_used[class.index()]
436 }
437}
438
439pub struct MemoryGovernor {
442 inner: Arc<Inner>,
443}
444
445impl Clone for MemoryGovernor {
446 fn clone(&self) -> Self {
447 Self {
448 inner: Arc::clone(&self.inner),
449 }
450 }
451}
452
453impl fmt::Debug for MemoryGovernor {
454 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455 f.debug_struct("MemoryGovernor")
456 .field("max_bytes", &self.inner.config.max_bytes)
457 .field("total_used", &self.inner.total_used.load(Ordering::Relaxed))
458 .field("pressure", &self.pressure())
459 .field("escalation", &self.escalation())
460 .finish()
461 }
462}
463
464impl MemoryGovernor {
465 pub fn new(config: GovernorConfig) -> Result<Self, MemoryError> {
467 config.validate()?;
468 Ok(Self {
469 inner: Arc::new(Inner {
470 config,
471 class_used: std::array::from_fn(|_| AtomicU64::new(0)),
472 total_used: AtomicU64::new(0),
473 escalation: AtomicU8::new(EscalationLevel::None as u8),
474 reservations_granted: AtomicU64::new(0),
475 reservations_rejected: AtomicU64::new(0),
476 low_priority_rejected: AtomicU64::new(0),
477 spill_triggers: AtomicU64::new(0),
478 reclaimers: parking_lot::Mutex::new(Vec::new()),
479 }),
480 })
481 }
482
483 pub fn with_max_bytes(max_bytes: u64) -> Result<Self, MemoryError> {
485 Self::new(GovernorConfig::new(max_bytes))
486 }
487
488 pub fn config(&self) -> &GovernorConfig {
490 &self.inner.config
491 }
492
493 pub fn max_bytes(&self) -> u64 {
495 self.inner.config.max_bytes
496 }
497
498 pub fn reserved_floor_bytes(&self) -> u64 {
500 self.inner.config.reserved_floor_bytes
501 }
502
503 pub fn class_budget(&self, class: MemoryClass) -> u64 {
505 self.inner.config.class_budgets[class.index()]
506 }
507
508 pub fn usage(&self, class: MemoryClass) -> u64 {
510 self.inner.class_used[class.index()].load(Ordering::Relaxed)
511 }
512
513 pub fn total_used(&self) -> u64 {
515 self.inner.total_used.load(Ordering::Relaxed)
516 }
517
518 pub fn pressure(&self) -> f64 {
520 let max = self.inner.config.max_bytes;
521 if max == 0 {
522 return 0.0;
523 }
524 (self.total_used() as f64 / max as f64).clamp(0.0, 1.0)
525 }
526
527 pub fn escalation(&self) -> EscalationLevel {
529 EscalationLevel::from_u8(self.inner.escalation.load(Ordering::Relaxed))
530 }
531
532 pub fn should_reject_low_priority_work(&self) -> bool {
534 self.escalation() >= EscalationLevel::RejectLowPriority
535 }
536
537 pub fn should_evict_caches(&self) -> bool {
540 self.escalation() >= EscalationLevel::EvictCaches
541 }
542
543 pub fn spill_trigger(&self) -> bool {
548 self.escalation() >= EscalationLevel::SpillOperators
549 }
550
551 pub fn should_throttle_maintenance(&self) -> bool {
553 self.escalation() >= EscalationLevel::ThrottleMaintenance
554 }
555
556 pub fn try_reserve(&self, bytes: u64, class: MemoryClass) -> Result<Reservation, MemoryError> {
571 self.inner.try_add(class, bytes)?;
572 Ok(Reservation {
573 governor: self.clone(),
574 class,
575 bytes,
576 })
577 }
578
579 pub fn register_reclaimable<R: Reclaimable + 'static>(&self, reclaimable: &Arc<R>) {
582 self.inner
583 .reclaimers
584 .lock()
585 .push(Arc::downgrade(reclaimable) as Weak<dyn Reclaimable>);
586 }
587
588 pub fn evict_reclaimable(&self, budget: u64) -> u64 {
592 let reclaimers: Vec<Arc<dyn Reclaimable>> = {
595 let mut registry = self.inner.reclaimers.lock();
596 registry.retain(|weak| weak.strong_count() > 0);
597 registry.iter().filter_map(Weak::upgrade).collect()
598 };
599 let mut freed = 0u64;
600 for reclaimer in reclaimers {
601 if freed >= budget {
602 break;
603 }
604 freed = freed.saturating_add(reclaimer.evict_reclaimable(budget - freed));
605 }
606 freed
607 }
608
609 pub fn reclaimable_bytes(&self) -> u64 {
611 let reclaimers: Vec<Arc<dyn Reclaimable>> = {
612 let registry = self.inner.reclaimers.lock();
613 registry.iter().filter_map(Weak::upgrade).collect()
614 };
615 reclaimers.iter().map(|r| r.reclaimable_bytes()).sum()
616 }
617
618 pub fn request_spill_grant(
630 &self,
631 reservation: &mut Reservation,
632 bytes: u64,
633 ) -> Option<SpillGrant> {
634 if !self.spill_trigger() || !reservation.class().is_spill_eligible() {
635 return None;
636 }
637 let bytes = bytes.min(reservation.bytes());
638 if bytes == 0 {
639 return None;
640 }
641 reservation
642 .resize(reservation.bytes() - bytes)
643 .expect("shrinking a reservation always succeeds");
644 Some(SpillGrant {
645 class: reservation.class(),
646 bytes,
647 })
648 }
649
650 pub fn stats(&self) -> GovernorStats {
652 GovernorStats {
653 max_bytes: self.inner.config.max_bytes,
654 reserved_floor_bytes: self.inner.config.reserved_floor_bytes,
655 total_used: self.total_used(),
656 class_used: std::array::from_fn(|i| self.inner.class_used[i].load(Ordering::Relaxed)),
657 pressure: self.pressure(),
658 escalation: self.escalation(),
659 reservations_granted: self.inner.reservations_granted.load(Ordering::Relaxed),
660 reservations_rejected: self.inner.reservations_rejected.load(Ordering::Relaxed),
661 low_priority_rejected: self.inner.low_priority_rejected.load(Ordering::Relaxed),
662 spill_triggers: self.inner.spill_triggers.load(Ordering::Relaxed),
663 }
664 }
665
666 fn recompute_escalation(&self) {
670 let pressure = self.pressure();
671 let thresholds = &self.inner.config.thresholds;
672 let mut level = self.escalation();
673 while let Some(next) = level.up() {
674 if pressure >= next.threshold(thresholds) {
675 level = next;
676 } else {
677 break;
678 }
679 }
680 while level != EscalationLevel::None
681 && pressure < level.threshold(thresholds) - thresholds.hysteresis
682 {
683 level = level.down();
684 }
685 let previous =
686 EscalationLevel::from_u8(self.inner.escalation.swap(level as u8, Ordering::Relaxed));
687 if previous < EscalationLevel::SpillOperators && level >= EscalationLevel::SpillOperators {
688 self.inner.spill_triggers.fetch_add(1, Ordering::Relaxed);
689 }
690 }
691}
692
693impl Inner {
694 fn try_add(self: &Arc<Self>, class: MemoryClass, bytes: u64) -> Result<(), MemoryError> {
698 if bytes == 0 {
699 return Ok(());
700 }
701 let governor = MemoryGovernor {
702 inner: Arc::clone(self),
703 };
704 if bytes > self.config.max_bytes {
708 self.reservations_rejected.fetch_add(1, Ordering::Relaxed);
709 return Err(MemoryError::Exhausted {
710 class,
711 requested: bytes,
712 available: 0,
713 });
714 }
715 if class.is_low_priority() && governor.escalation() >= EscalationLevel::RejectLowPriority {
716 self.low_priority_rejected.fetch_add(1, Ordering::Relaxed);
717 self.reservations_rejected.fetch_add(1, Ordering::Relaxed);
718 return Err(MemoryError::LowPriorityRejected { class });
719 }
720 let index = class.index();
721 let new_class_used = self.class_used[index].fetch_add(bytes, Ordering::Relaxed) + bytes;
722 let new_total_used = self.total_used.fetch_add(bytes, Ordering::Relaxed) + bytes;
723 let limit = if class.is_reserved() {
724 self.config.max_bytes
725 } else {
726 self.config.max_bytes - self.config.reserved_floor_bytes
727 };
728 if new_class_used <= self.config.class_budgets[index] && new_total_used <= limit {
729 self.reservations_granted.fetch_add(1, Ordering::Relaxed);
730 governor.recompute_escalation();
731 Ok(())
732 } else {
733 self.class_used[index].fetch_sub(bytes, Ordering::Relaxed);
734 self.total_used.fetch_sub(bytes, Ordering::Relaxed);
735 self.reservations_rejected.fetch_add(1, Ordering::Relaxed);
736 Err(MemoryError::Exhausted {
737 class,
738 requested: bytes,
739 available: limit.saturating_sub(new_total_used - bytes),
740 })
741 }
742 }
743
744 fn release(self: &Arc<Self>, class: MemoryClass, bytes: u64) {
747 if bytes == 0 {
748 return;
749 }
750 self.class_used[class.index()].fetch_sub(bytes, Ordering::Relaxed);
751 self.total_used.fetch_sub(bytes, Ordering::Relaxed);
752 MemoryGovernor {
753 inner: Arc::clone(self),
754 }
755 .recompute_escalation();
756 }
757}
758
759#[must_use = "a reservation releases its bytes on drop"]
765pub struct Reservation {
766 governor: MemoryGovernor,
767 class: MemoryClass,
768 bytes: u64,
769}
770
771impl Reservation {
772 pub fn class(&self) -> MemoryClass {
774 self.class
775 }
776
777 pub fn bytes(&self) -> u64 {
779 self.bytes
780 }
781
782 pub fn resize(&mut self, new_bytes: u64) -> Result<(), MemoryError> {
787 if new_bytes == self.bytes {
788 return Ok(());
789 }
790 if new_bytes < self.bytes {
791 let delta = self.bytes - new_bytes;
792 self.governor.inner.release(self.class, delta);
793 self.bytes = new_bytes;
794 Ok(())
795 } else {
796 self.governor
797 .inner
798 .try_add(self.class, new_bytes - self.bytes)?;
799 self.bytes = new_bytes;
800 Ok(())
801 }
802 }
803}
804
805impl Drop for Reservation {
806 fn drop(&mut self) {
807 self.governor.inner.release(self.class, self.bytes);
808 }
809}
810
811impl fmt::Debug for Reservation {
812 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813 f.debug_struct("Reservation")
814 .field("class", &self.class)
815 .field("bytes", &self.bytes)
816 .finish()
817 }
818}
819
820#[derive(Debug, Clone, Copy, PartialEq, Eq)]
826pub struct SpillGrant {
827 class: MemoryClass,
828 bytes: u64,
829}
830
831impl SpillGrant {
832 pub fn class(self) -> MemoryClass {
834 self.class
835 }
836
837 pub fn bytes(self) -> u64 {
839 self.bytes
840 }
841}
842
843#[cfg(test)]
844mod tests {
845 use super::*;
846
847 fn governor(max: u64, floor: u64) -> MemoryGovernor {
848 MemoryGovernor::new(GovernorConfig::new(max).with_reserved_floor(floor)).unwrap()
849 }
850
851 #[test]
852 fn memory_class_set_matches_spec() {
853 assert_eq!(MemoryClass::ALL.len(), 9);
855 assert_eq!(MemoryClass::COUNT, 9);
856 let names: Vec<_> = MemoryClass::ALL.iter().map(|c| c.name()).collect();
857 assert_eq!(
858 names,
859 vec![
860 "page_cache",
861 "decoded_cache",
862 "query_execution",
863 "result_buffering",
864 "ai_candidates",
865 "compaction",
866 "replication",
867 "backup",
868 "network_buffers"
869 ]
870 );
871 for (i, class) in MemoryClass::ALL.iter().enumerate() {
872 assert_eq!(class.index(), i);
873 }
874 }
875
876 #[test]
877 fn class_classification() {
878 for class in MemoryClass::ALL {
879 assert_eq!(
880 class.is_reserved(),
881 matches!(
882 class,
883 MemoryClass::Replication | MemoryClass::NetworkBuffers
884 ),
885 "reserved: {class}"
886 );
887 assert_eq!(
888 class.is_low_priority(),
889 matches!(class, MemoryClass::Compaction | MemoryClass::Backup),
890 "low priority: {class}"
891 );
892 assert_eq!(
893 class.is_reclaimable_cache(),
894 matches!(class, MemoryClass::PageCache | MemoryClass::DecodedCache),
895 "reclaimable: {class}"
896 );
897 }
898 }
899
900 #[test]
901 fn reservation_accounting_and_raii_release() {
902 let governor = governor(1024, 128);
903 assert_eq!(governor.total_used(), 0);
904 assert_eq!(governor.pressure(), 0.0);
905
906 let a = governor
907 .try_reserve(100, MemoryClass::QueryExecution)
908 .unwrap();
909 let b = governor.try_reserve(60, MemoryClass::PageCache).unwrap();
910 assert_eq!(governor.usage(MemoryClass::QueryExecution), 100);
911 assert_eq!(governor.usage(MemoryClass::PageCache), 60);
912 assert_eq!(governor.total_used(), 160);
913 assert!((governor.pressure() - 160.0 / 1024.0).abs() < 1e-12);
914 assert_eq!(a.bytes(), 100);
915 assert_eq!(a.class(), MemoryClass::QueryExecution);
916
917 drop(a);
918 assert_eq!(governor.usage(MemoryClass::QueryExecution), 0);
919 assert_eq!(governor.total_used(), 60);
920
921 let stats_before = governor.stats();
923 drop(b);
924 assert_eq!(governor.total_used(), 0);
925 assert_eq!(governor.pressure(), 0.0);
926 assert_eq!(
927 governor.stats().reservations_granted,
928 stats_before.reservations_granted
929 );
930 }
931
932 #[test]
933 fn resize_grows_and_shrinks_with_admission_rules() {
934 let governor = governor(1000, 100);
935 let mut r = governor
936 .try_reserve(100, MemoryClass::QueryExecution)
937 .unwrap();
938 r.resize(900).unwrap();
939 assert_eq!(r.bytes(), 900);
940 assert_eq!(governor.total_used(), 900);
941 assert!(r.resize(901).is_err());
944 assert_eq!(r.bytes(), 900);
945 assert_eq!(governor.total_used(), 900);
946 r.resize(50).unwrap();
948 assert_eq!(governor.total_used(), 50);
949 r.resize(0).unwrap();
950 assert_eq!(governor.total_used(), 0);
951 assert!(r.resize(0).is_ok());
952 }
953
954 #[test]
955 fn per_class_budget_is_enforced() {
956 let governor = MemoryGovernor::new(
957 GovernorConfig::new(1000).with_class_budget(MemoryClass::AiCandidates, 100),
958 )
959 .unwrap();
960 let _held = governor
961 .try_reserve(100, MemoryClass::AiCandidates)
962 .unwrap();
963 let rejected = governor.try_reserve(1, MemoryClass::AiCandidates);
964 assert!(matches!(rejected, Err(MemoryError::Exhausted { .. })));
965 assert!(governor
967 .try_reserve(500, MemoryClass::QueryExecution)
968 .is_ok());
969 }
970
971 #[test]
972 fn zero_byte_reservations_always_succeed() {
973 let governor = governor(100, 10);
974 let _full = governor.try_reserve(100, MemoryClass::Replication).unwrap();
975 for class in MemoryClass::ALL {
977 assert!(
978 governor.try_reserve(0, class).is_ok(),
979 "zero bytes: {class}"
980 );
981 }
982 }
983
984 #[test]
985 fn oversized_requests_are_rejected_without_touching_counters() {
986 let governor = governor(100, 10);
987 for class in MemoryClass::ALL {
988 let rejected = governor.try_reserve(u64::MAX, class);
989 assert!(
990 matches!(rejected, Err(MemoryError::Exhausted { .. })),
991 "oversized: {class}"
992 );
993 }
994 assert_eq!(governor.total_used(), 0);
995 for class in MemoryClass::ALL {
996 assert_eq!(governor.usage(class), 0, "{class}");
997 }
998 assert_eq!(governor.stats().reservations_rejected, 9);
999 assert_eq!(governor.stats().reservations_granted, 0);
1000 assert!(governor.try_reserve(50, MemoryClass::Replication).is_ok());
1002 }
1003
1004 #[test]
1005 fn reserved_floor_never_starved_under_adversarial_pressure() {
1006 let governor = governor(1000, 100);
1007 let mut held = Vec::new();
1010 for _ in 0..9 {
1011 held.push(
1012 governor
1013 .try_reserve(100, MemoryClass::QueryExecution)
1014 .unwrap(),
1015 );
1016 }
1017 for class in MemoryClass::ALL {
1021 if class.is_reserved() {
1022 continue;
1023 }
1024 let rejected = governor.try_reserve(1, class);
1025 if class.is_low_priority() {
1026 assert!(
1027 matches!(rejected, Err(MemoryError::LowPriorityRejected { .. })),
1028 "low-priority {class} rejected under step 1"
1029 );
1030 } else {
1031 assert!(
1032 matches!(rejected, Err(MemoryError::Exhausted { .. })),
1033 "non-reserved {class} must stop at the floor"
1034 );
1035 }
1036 }
1037 held.push(governor.try_reserve(60, MemoryClass::Replication).unwrap());
1040 held.push(
1041 governor
1042 .try_reserve(40, MemoryClass::NetworkBuffers)
1043 .unwrap(),
1044 );
1045 assert_eq!(governor.total_used(), 1000);
1046 assert!(governor.try_reserve(1, MemoryClass::Replication).is_err());
1048 assert!(governor
1049 .try_reserve(1, MemoryClass::NetworkBuffers)
1050 .is_err());
1051 held.clear();
1053 assert_eq!(governor.total_used(), 0);
1054 assert!(governor
1055 .try_reserve(900, MemoryClass::QueryExecution)
1056 .is_ok());
1057 }
1058
1059 #[test]
1060 fn escalation_levels_activate_in_exact_spec_order() {
1061 let governor = governor(1000, 0);
1064 let mut held = Vec::new();
1065 let reserve = |bytes: u64, held: &mut Vec<Reservation>| {
1066 held.push(
1067 governor
1068 .try_reserve(bytes, MemoryClass::QueryExecution)
1069 .unwrap(),
1070 );
1071 };
1072
1073 assert_eq!(governor.escalation(), EscalationLevel::None);
1074 reserve(600, &mut held); assert_eq!(governor.escalation(), EscalationLevel::None);
1076 assert!(!governor.should_reject_low_priority_work());
1077
1078 reserve(100, &mut held); assert_eq!(governor.escalation(), EscalationLevel::RejectLowPriority);
1080 assert!(governor.should_reject_low_priority_work());
1081 assert!(!governor.should_evict_caches());
1082
1083 reserve(100, &mut held); assert_eq!(governor.escalation(), EscalationLevel::EvictCaches);
1085 assert!(governor.should_evict_caches());
1086 assert!(!governor.spill_trigger());
1087
1088 reserve(100, &mut held); assert_eq!(governor.escalation(), EscalationLevel::SpillOperators);
1090 assert!(governor.spill_trigger());
1091 assert!(!governor.should_throttle_maintenance());
1092 assert_eq!(governor.stats().spill_triggers, 1);
1093
1094 reserve(50, &mut held); assert_eq!(governor.escalation(), EscalationLevel::ThrottleMaintenance);
1096 assert!(governor.should_throttle_maintenance());
1097 assert_eq!(governor.stats().spill_triggers, 1);
1098
1099 assert!(EscalationLevel::RejectLowPriority < EscalationLevel::EvictCaches);
1101 assert!(EscalationLevel::EvictCaches < EscalationLevel::SpillOperators);
1102 assert!(EscalationLevel::SpillOperators < EscalationLevel::ThrottleMaintenance);
1103 }
1104
1105 #[test]
1106 fn hysteresis_prevents_flapping() {
1107 let governor = governor(1000, 0);
1108 let mut r = governor
1109 .try_reserve(700, MemoryClass::QueryExecution)
1110 .unwrap();
1111 assert_eq!(governor.escalation(), EscalationLevel::RejectLowPriority);
1112 r.resize(660).unwrap();
1115 assert_eq!(governor.escalation(), EscalationLevel::RejectLowPriority);
1116 r.resize(640).unwrap();
1118 assert_eq!(governor.escalation(), EscalationLevel::None);
1119 r.resize(800).unwrap();
1122 assert_eq!(governor.escalation(), EscalationLevel::EvictCaches);
1123 r.resize(760).unwrap();
1124 assert_eq!(governor.escalation(), EscalationLevel::EvictCaches);
1125 r.resize(740).unwrap();
1126 assert_eq!(governor.escalation(), EscalationLevel::RejectLowPriority);
1127 }
1128
1129 #[test]
1130 fn spill_eligible_classes_are_the_query_pools() {
1131 for class in MemoryClass::ALL {
1132 assert_eq!(
1133 class.is_spill_eligible(),
1134 matches!(
1135 class,
1136 MemoryClass::QueryExecution | MemoryClass::ResultBuffering
1137 ),
1138 "spill-eligible: {class}"
1139 );
1140 }
1141 }
1142
1143 #[test]
1144 fn spill_grants_require_the_trigger_and_an_eligible_class() {
1145 let governor = governor(1000, 0);
1146 let mut query = governor
1147 .try_reserve(100, MemoryClass::QueryExecution)
1148 .unwrap();
1149 assert!(!governor.spill_trigger());
1151 assert_eq!(governor.request_spill_grant(&mut query, 50), None);
1152 assert_eq!(query.bytes(), 100);
1153
1154 let mut cache = governor.try_reserve(800, MemoryClass::PageCache).unwrap();
1156 assert!(governor.spill_trigger());
1157 assert_eq!(governor.request_spill_grant(&mut cache, 50), None);
1159 assert_eq!(cache.bytes(), 800);
1160 let mut replication = governor.try_reserve(90, MemoryClass::Replication).unwrap();
1161 assert_eq!(governor.request_spill_grant(&mut replication, 50), None);
1162 let grant = governor.request_spill_grant(&mut query, 60).unwrap();
1164 assert_eq!(
1165 grant,
1166 SpillGrant {
1167 class: MemoryClass::QueryExecution,
1168 bytes: 60
1169 }
1170 );
1171 assert_eq!(grant.class(), MemoryClass::QueryExecution);
1172 assert_eq!(grant.bytes(), 60);
1173 }
1174
1175 #[test]
1176 fn spill_grant_shrinks_the_reservation_and_clamps_to_held_bytes() {
1177 let governor = governor(1000, 0);
1178 let mut r = governor
1179 .try_reserve(900, MemoryClass::QueryExecution)
1180 .unwrap();
1181 assert!(governor.spill_trigger());
1182 assert_eq!(governor.total_used(), 900);
1183
1184 let grant = governor.request_spill_grant(&mut r, 400).unwrap();
1185 assert_eq!(grant.bytes(), 400);
1186 assert_eq!(r.bytes(), 500);
1187 assert_eq!(governor.total_used(), 500);
1190 assert!(!governor.spill_trigger());
1191 assert_eq!(governor.request_spill_grant(&mut r, 100), None);
1192
1193 let _pressure = governor
1195 .try_reserve(400, MemoryClass::QueryExecution)
1196 .unwrap();
1197 assert!(governor.spill_trigger());
1198 let grant = governor.request_spill_grant(&mut r, u64::MAX).unwrap();
1199 assert_eq!(grant.bytes(), 500);
1200 assert_eq!(r.bytes(), 0);
1201 assert_eq!(governor.total_used(), 400);
1202 let _more_pressure = governor
1204 .try_reserve(500, MemoryClass::QueryExecution)
1205 .unwrap();
1206 assert!(governor.spill_trigger());
1207 assert_eq!(governor.request_spill_grant(&mut r, 1), None);
1208
1209 drop(_more_pressure);
1212 drop(_pressure);
1213 assert!(governor
1214 .try_reserve(500, MemoryClass::QueryExecution)
1215 .is_ok());
1216 }
1217
1218 #[test]
1219 fn low_priority_work_is_rejected_first_under_pressure() {
1220 let governor = governor(1000, 100);
1221 let _pressure = governor
1222 .try_reserve(700, MemoryClass::QueryExecution)
1223 .unwrap();
1224 assert!(governor.should_reject_low_priority_work());
1225 for class in [MemoryClass::Compaction, MemoryClass::Backup] {
1227 let rejected = governor.try_reserve(1, class);
1228 assert!(
1229 matches!(rejected, Err(MemoryError::LowPriorityRejected { .. })),
1230 "{class} rejected under step 1"
1231 );
1232 }
1233 assert_eq!(governor.stats().low_priority_rejected, 2);
1234 assert!(governor
1236 .try_reserve(100, MemoryClass::QueryExecution)
1237 .is_ok());
1238 drop(_pressure);
1239 assert!(governor.try_reserve(100, MemoryClass::Compaction).is_ok());
1241 }
1242
1243 struct StubReclaimer {
1244 bytes: AtomicU64,
1245 }
1246
1247 impl Reclaimable for StubReclaimer {
1248 fn evict_reclaimable(&self, budget: u64) -> u64 {
1249 let freed = self.bytes.load(Ordering::Relaxed).min(budget);
1250 self.bytes.fetch_sub(freed, Ordering::Relaxed);
1251 freed
1252 }
1253
1254 fn reclaimable_bytes(&self) -> u64 {
1255 self.bytes.load(Ordering::Relaxed)
1256 }
1257 }
1258
1259 #[test]
1260 fn evict_reclaimable_drives_registered_subsystems() {
1261 let governor = governor(1000, 100);
1262 let a = Arc::new(StubReclaimer {
1263 bytes: AtomicU64::new(60),
1264 });
1265 let b = Arc::new(StubReclaimer {
1266 bytes: AtomicU64::new(100),
1267 });
1268 governor.register_reclaimable(&a);
1269 governor.register_reclaimable(&b);
1270 assert_eq!(governor.reclaimable_bytes(), 160);
1271
1272 assert_eq!(governor.evict_reclaimable(100), 100);
1274 assert_eq!(a.reclaimable_bytes(), 0);
1275 assert_eq!(b.reclaimable_bytes(), 60);
1276 assert_eq!(governor.evict_reclaimable(1000), 60);
1278 drop(a);
1280 drop(b);
1281 assert_eq!(governor.reclaimable_bytes(), 0);
1282 assert_eq!(governor.evict_reclaimable(10), 0);
1283 assert!(governor.inner.reclaimers.lock().is_empty());
1284 }
1285
1286 #[test]
1287 fn config_validation() {
1288 assert!(matches!(
1289 MemoryGovernor::new(GovernorConfig::new(0)),
1290 Err(MemoryError::InvalidConfig(_))
1291 ));
1292 assert!(matches!(
1293 MemoryGovernor::new(GovernorConfig::new(100).with_reserved_floor(101)),
1294 Err(MemoryError::InvalidConfig(_))
1295 ));
1296 assert!(matches!(
1297 MemoryGovernor::new(
1298 GovernorConfig::new(100).with_class_budget(MemoryClass::Backup, 101)
1299 ),
1300 Err(MemoryError::InvalidConfig(_))
1301 ));
1302 let not_increasing = EscalationThresholds {
1304 reject_low_priority: 0.70,
1305 evict_caches: 0.70,
1306 spill_operators: 0.90,
1307 throttle_maintenance: 0.95,
1308 hysteresis: 0.05,
1309 };
1310 assert!(matches!(
1311 MemoryGovernor::new(GovernorConfig::new(100).with_thresholds(not_increasing)),
1312 Err(MemoryError::InvalidConfig(_))
1313 ));
1314 let band_too_wide = EscalationThresholds {
1316 hysteresis: 0.70,
1317 ..EscalationThresholds::default()
1318 };
1319 assert!(matches!(
1320 MemoryGovernor::new(GovernorConfig::new(100).with_thresholds(band_too_wide)),
1321 Err(MemoryError::InvalidConfig(_))
1322 ));
1323 let out_of_band = EscalationThresholds {
1325 throttle_maintenance: 1.5,
1326 ..EscalationThresholds::default()
1327 };
1328 assert!(matches!(
1329 MemoryGovernor::new(GovernorConfig::new(100).with_thresholds(out_of_band)),
1330 Err(MemoryError::InvalidConfig(_))
1331 ));
1332 assert!(MemoryGovernor::new(GovernorConfig::new(100)).is_ok());
1333 }
1334
1335 #[test]
1336 fn concurrent_reservations_never_exceed_limits_and_release_exactly() {
1337 let governor = governor(1 << 16, 1 << 12);
1338 let mut granted_total = 0u64;
1339 let mut held = Vec::new();
1342 while let Ok(r) = governor.try_reserve(64, MemoryClass::QueryExecution) {
1343 granted_total += 64;
1344 held.push(r);
1345 }
1346 assert!(granted_total <= (1 << 16) - (1 << 12));
1347 held.clear();
1348 assert_eq!(governor.total_used(), 0);
1349
1350 let governor = Arc::new(governor);
1353 let mut threads = Vec::new();
1354 for _ in 0..4 {
1355 let governor = Arc::clone(&governor);
1356 threads.push(std::thread::spawn(move || {
1357 for _ in 0..1000 {
1358 if let Ok(r) = governor.try_reserve(128, MemoryClass::QueryExecution) {
1359 std::thread::yield_now();
1360 drop(r);
1361 }
1362 }
1363 }));
1364 }
1365 for t in threads {
1366 t.join().unwrap();
1367 }
1368 assert_eq!(governor.total_used(), 0);
1369 for class in MemoryClass::ALL {
1370 assert_eq!(governor.usage(class), 0, "{class}");
1371 }
1372 let full = governor
1373 .try_reserve(((1 << 16) - (1 << 12)) as u64, MemoryClass::QueryExecution)
1374 .unwrap();
1375 assert_eq!(governor.total_used(), (1 << 16) - (1 << 12));
1376 drop(full);
1377 assert_eq!(governor.total_used(), 0);
1378 }
1379
1380 #[test]
1381 fn stats_snapshot_is_consistent() {
1382 let governor = governor(2048, 256);
1383 let _a = governor.try_reserve(512, MemoryClass::PageCache).unwrap();
1384 let _b = governor.try_reserve(128, MemoryClass::Replication).unwrap();
1385 let _ = governor.try_reserve(1 << 20, MemoryClass::Backup); let stats = governor.stats();
1387 assert_eq!(stats.max_bytes, 2048);
1388 assert_eq!(stats.reserved_floor_bytes, 256);
1389 assert_eq!(stats.total_used, 640);
1390 assert_eq!(stats.usage_for(MemoryClass::PageCache), 512);
1391 assert_eq!(stats.usage_for(MemoryClass::Replication), 128);
1392 assert!((stats.pressure - 640.0 / 2048.0).abs() < 1e-12);
1393 assert_eq!(stats.escalation, governor.escalation());
1394 assert_eq!(stats.reservations_granted, 2);
1395 assert_eq!(stats.reservations_rejected, 1);
1396 }
1397
1398 #[test]
1399 fn config_serde_round_trip() {
1400 let config = GovernorConfig::new(1 << 20)
1401 .with_reserved_floor(1 << 16)
1402 .with_class_budget(MemoryClass::AiCandidates, 1 << 17);
1403 let json = serde_json::to_string(&config).unwrap();
1404 let back: GovernorConfig = serde_json::from_str(&json).unwrap();
1405 assert_eq!(back, config);
1406 assert!(back.validate().is_ok());
1407 }
1408}