Skip to main content

mongreldb_core/
memory.rs

1//! Global memory governor (spec section 10.5, S1E-003).
2//!
3//! Implemented in the Stage 1E wave: one node-level [`MemoryGovernor`] owns the
4//! budgets of S1E-003 — page cache, decoded cache, query execution, result
5//! buffering, AI candidates, compaction, replication, backup, and network
6//! buffers — so no subsystem independently allocates beyond its reservation
7//! (spec §13.2). Subsystems reserve through
8//! [`try_reserve`](MemoryGovernor::try_reserve) and hold the returned
9//! [`Reservation`] RAII guard; dropping the guard releases the bytes.
10//!
11//! ## Pressure and escalation
12//!
13//! [`pressure`](MemoryGovernor::pressure) is the fraction of the configured
14//! maximum in use (`0.0..=1.0`). As it rises the governor escalates in the
15//! exact order of S1E-003, each level with its own threshold and hysteresis so
16//! the level does not flap around a boundary:
17//!
18//! 1. [`RejectLowPriority`](EscalationLevel::RejectLowPriority) — new
19//!    low-priority work is rejected in `try_reserve`.
20//! 2. [`EvictCaches`](EscalationLevel::EvictCaches) — the governor drives
21//!    registered reclaimable caches via [`evict_reclaimable`](MemoryGovernor::evict_reclaimable).
22//! 3. [`SpillOperators`](EscalationLevel::SpillOperators) — eligible query
23//!    operators spill working memory to disk: [`spill_trigger`](MemoryGovernor::spill_trigger)
24//!    exposes the signal and [`request_spill_grant`](MemoryGovernor::request_spill_grant)
25//!    issues the S1E-004 [`SpillGrant`] (the disk side is
26//!    [`crate::spill::SpillManager`]).
27//! 4. [`ThrottleMaintenance`](EscalationLevel::ThrottleMaintenance) —
28//!    maintenance work is throttled (§13.1: maintenance yields to foreground).
29//! 5. At every level the reserved floor holds: replication and network-buffer
30//!    (control-plane/replication protocol, §13.1) memory is never fully
31//!    starved — non-reserved classes can never consume the last
32//!    `reserved_floor_bytes`.
33//!
34//! The reservation fast path is lock-free (a pair of atomic adds with exact
35//! rollback on rejection) and performs no allocation; the [`Reservation`]
36//! guard is two words plus one `Arc` refcount bump.
37
38use std::fmt;
39use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
40use std::sync::{Arc, Weak};
41
42use serde::{Deserialize, Serialize};
43
44/// The memory pools of S1E-003: the budgets one node-level governor owns.
45///
46/// The variant set is exactly the spec's, in spec order; [`index`](Self::index)
47/// matches that order and is the layout of
48/// [`GovernorConfig::class_budgets`].
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
50pub enum MemoryClass {
51    /// Raw (on-disk form) page cache.
52    PageCache,
53    /// Decoded (post-decompress/decrypt) page cache.
54    DecodedCache,
55    /// Query execution operators (hash tables, sort runs in memory).
56    QueryExecution,
57    /// Result materialization and buffering.
58    ResultBuffering,
59    /// AI candidate sets (ANN/full-text retrieval buffers).
60    AiCandidates,
61    /// Compaction and index maintenance.
62    Compaction,
63    /// Replication log shipping and follow-apply buffers. Reserved.
64    Replication,
65    /// Backup/export buffers.
66    Backup,
67    /// Network receive/send buffers; carries the control-plane and
68    /// replication protocols. Reserved.
69    NetworkBuffers,
70}
71
72impl MemoryClass {
73    /// Number of classes (array layout of the governor's counters/budgets).
74    pub const COUNT: usize = 9;
75
76    /// Every class, in spec order (the [`index`](Self::index) layout).
77    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    /// Stable index in `0..COUNT` (spec order).
90    pub fn index(self) -> usize {
91        Self::ALL
92            .iter()
93            .position(|c| *c == self)
94            .expect("ALL is total")
95    }
96
97    /// Stable lowercase name.
98    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    /// Classes whose memory must never be fully starved (S1E-003 step 5,
113    /// §13.1 reserved control/replication capacity): the replication pool, and
114    /// the network buffers that carry the control-plane and replication
115    /// protocols. Only these may draw down the reserved floor.
116    pub fn is_reserved(self) -> bool {
117        matches!(self, MemoryClass::Replication | MemoryClass::NetworkBuffers)
118    }
119
120    /// Deferrable classes rejected first under pressure (S1E-003 step 1,
121    /// §13.1 maintenance yields to foreground work).
122    pub fn is_low_priority(self) -> bool {
123        matches!(self, MemoryClass::Compaction | MemoryClass::Backup)
124    }
125
126    /// Classes whose memory is reclaimable on demand (S1E-003 step 2): the
127    /// caches can evict entries and hand bytes back.
128    pub fn is_reclaimable_cache(self) -> bool {
129        matches!(self, MemoryClass::PageCache | MemoryClass::DecodedCache)
130    }
131
132    /// Classes whose operator working memory the S1E-004 spill manager can
133    /// move to disk under escalation step 3: query execution (hash tables,
134    /// sort runs) and result materialization. The caches are reclaimed
135    /// instead (step 2), AI candidate sets are recomputable, and the reserved
136    /// pools are never spilled.
137    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/// Errors of governor configuration and reservation.
152#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
153pub enum MemoryError {
154    /// The [`GovernorConfig`] failed validation.
155    #[error("invalid memory governor configuration: {0}")]
156    InvalidConfig(&'static str),
157    /// The reservation did not fit within the class budget, the node maximum,
158    /// or (for non-reserved classes) the reserved floor.
159    #[error(
160        "memory reservation of {requested} bytes for {class} rejected: {available} bytes available"
161    )]
162    Exhausted {
163        /// Requesting pool.
164        class: MemoryClass,
165        /// Requested bytes.
166        requested: u64,
167        /// Bytes the requester could still have been granted.
168        available: u64,
169    },
170    /// Low-priority work rejected under pressure (S1E-003 escalation step 1).
171    #[error("low-priority memory reservation for {class} rejected: memory pressure (S1E-003 escalation step 1)")]
172    LowPriorityRejected {
173        /// Requesting pool.
174        class: MemoryClass,
175    },
176}
177
178/// The pressure thresholds at which each escalation level activates, plus the
179/// hysteresis band that keeps the level from flapping around a boundary.
180///
181/// Fractions of the configured maximum, in `(0, 1]`, strictly increasing in
182/// the S1E-003 escalation order.
183#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
184pub struct EscalationThresholds {
185    /// Step 1: reject new low-priority work.
186    pub reject_low_priority: f64,
187    /// Step 2: evict reclaimable caches.
188    pub evict_caches: f64,
189    /// Step 3: spill eligible query operators (S1E-004 hook point).
190    pub spill_operators: f64,
191    /// Step 4: throttle maintenance.
192    pub throttle_maintenance: f64,
193    /// De-escalation band: a level drops only once pressure falls below its
194    /// activation threshold minus this hysteresis.
195    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/// Node-level governor configuration.
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
241pub struct GovernorConfig {
242    /// Configured maximum bytes the node may reserve across all classes.
243    pub max_bytes: u64,
244    /// Bytes of `max_bytes` only reserved classes (replication, network
245    /// buffers) may consume (S1E-003 step 5).
246    pub reserved_floor_bytes: u64,
247    /// Per-class budgets in [`MemoryClass::index`] order. Defaults to
248    /// `max_bytes` per class (bounded by the node total only).
249    pub class_budgets: [u64; MemoryClass::COUNT],
250    /// Escalation thresholds.
251    pub thresholds: EscalationThresholds,
252}
253
254impl GovernorConfig {
255    /// A config with the default reserved floor (`max_bytes / 8`), per-class
256    /// budgets bounded only by the node total, and default thresholds.
257    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    /// Overrides one class budget.
267    pub fn with_class_budget(mut self, class: MemoryClass, bytes: u64) -> Self {
268        self.class_budgets[class.index()] = bytes;
269        self
270    }
271
272    /// Overrides the reserved floor.
273    pub fn with_reserved_floor(mut self, bytes: u64) -> Self {
274        self.reserved_floor_bytes = bytes;
275        self
276    }
277
278    /// Overrides the escalation thresholds.
279    pub fn with_thresholds(mut self, thresholds: EscalationThresholds) -> Self {
280        self.thresholds = thresholds;
281        self
282    }
283
284    /// Checks the configuration invariants.
285    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/// The governor's escalation level under memory pressure, in the exact order
304/// of S1E-003 (a higher level implies every lower level's action stays
305/// active). Ordering is derived, so levels compare by escalation severity.
306#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
307pub enum EscalationLevel {
308    /// No pressure response.
309    None = 0,
310    /// Step 1: reject new low-priority work.
311    RejectLowPriority = 1,
312    /// Step 2: evict reclaimable caches.
313    EvictCaches = 2,
314    /// Step 3: spill eligible query operators (S1E-004 hook point).
315    SpillOperators = 3,
316    /// Step 4: throttle maintenance.
317    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    /// The next more severe level, if any.
332    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    /// The next less severe level.
343    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    /// The pressure at which this level activates.
354    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    /// Stable name.
365    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
382/// A subsystem whose memory the governor can reclaim under pressure (S1E-003
383/// step 2). Implemented by the page caches (`crate::cache`); the governor
384/// holds implementors weakly, so a dropped cache simply stops being driven.
385pub trait Reclaimable: Send + Sync {
386    /// Evict at least `budget` bytes of reclaimable entries; returns the bytes
387    /// actually freed (fewer when less was reclaimable).
388    fn evict_reclaimable(&self, budget: u64) -> u64;
389    /// Bytes currently reclaimable.
390    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    /// Reclaimable subsystems (cold path only — registration and pressure
403    /// relief; never touched by the reservation fast path).
404    reclaimers: parking_lot::Mutex<Vec<Weak<dyn Reclaimable>>>,
405}
406
407/// A point-in-time snapshot of governor state (telemetry and tests).
408#[derive(Debug, Clone)]
409pub struct GovernorStats {
410    /// Configured node maximum.
411    pub max_bytes: u64,
412    /// Configured reserved floor.
413    pub reserved_floor_bytes: u64,
414    /// Total reserved bytes across all classes.
415    pub total_used: u64,
416    /// Reserved bytes per class, in [`MemoryClass::index`] order.
417    pub class_used: [u64; MemoryClass::COUNT],
418    /// `total_used / max_bytes`, clamped to `0.0..=1.0`.
419    pub pressure: f64,
420    /// Current escalation level.
421    pub escalation: EscalationLevel,
422    /// Cumulative granted reservations.
423    pub reservations_granted: u64,
424    /// Cumulative rejected reservations.
425    pub reservations_rejected: u64,
426    /// Cumulative rejections under escalation step 1.
427    pub low_priority_rejected: u64,
428    /// Cumulative entries into the spill level (S1E-004 hook signal count).
429    pub spill_triggers: u64,
430}
431
432impl GovernorStats {
433    /// Reserved bytes of one class.
434    pub fn usage_for(&self, class: MemoryClass) -> u64 {
435        self.class_used[class.index()]
436    }
437}
438
439/// The node-level memory governor (S1E-003). Cheap to clone (one `Arc`);
440/// thread-safe; the reservation fast path is lock-free and allocation-free.
441pub 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    /// A governor over `config` (validated).
466    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    /// A governor with the default configuration for `max_bytes`.
484    pub fn with_max_bytes(max_bytes: u64) -> Result<Self, MemoryError> {
485        Self::new(GovernorConfig::new(max_bytes))
486    }
487
488    /// The governor's configuration.
489    pub fn config(&self) -> &GovernorConfig {
490        &self.inner.config
491    }
492
493    /// Configured node maximum in bytes.
494    pub fn max_bytes(&self) -> u64 {
495        self.inner.config.max_bytes
496    }
497
498    /// Configured reserved floor in bytes.
499    pub fn reserved_floor_bytes(&self) -> u64 {
500        self.inner.config.reserved_floor_bytes
501    }
502
503    /// Configured budget of one class.
504    pub fn class_budget(&self, class: MemoryClass) -> u64 {
505        self.inner.config.class_budgets[class.index()]
506    }
507
508    /// Currently reserved bytes of one class.
509    pub fn usage(&self, class: MemoryClass) -> u64 {
510        self.inner.class_used[class.index()].load(Ordering::Relaxed)
511    }
512
513    /// Currently reserved bytes across all classes.
514    pub fn total_used(&self) -> u64 {
515        self.inner.total_used.load(Ordering::Relaxed)
516    }
517
518    /// Fraction of the configured maximum in use, clamped to `0.0..=1.0`.
519    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    /// The current escalation level (with hysteresis applied).
528    pub fn escalation(&self) -> EscalationLevel {
529        EscalationLevel::from_u8(self.inner.escalation.load(Ordering::Relaxed))
530    }
531
532    /// Step 1 active: new low-priority work is being rejected.
533    pub fn should_reject_low_priority_work(&self) -> bool {
534        self.escalation() >= EscalationLevel::RejectLowPriority
535    }
536
537    /// Step 2 active: reclaimable caches should be evicted
538    /// ([`evict_reclaimable`](Self::evict_reclaimable) drives them).
539    pub fn should_evict_caches(&self) -> bool {
540        self.escalation() >= EscalationLevel::EvictCaches
541    }
542
543    /// Step 3 active: eligible query operators should spill. Exposed to the
544    /// S1E-004 spill path: [`request_spill_grant`](Self::request_spill_grant)
545    /// turns the signal into a typed grant; the disk side is
546    /// [`crate::spill::SpillManager`].
547    pub fn spill_trigger(&self) -> bool {
548        self.escalation() >= EscalationLevel::SpillOperators
549    }
550
551    /// Step 4 active: maintenance work should be throttled (§13.1).
552    pub fn should_throttle_maintenance(&self) -> bool {
553        self.escalation() >= EscalationLevel::ThrottleMaintenance
554    }
555
556    /// Reserves `bytes` of `class`, returning an RAII guard that releases the
557    /// bytes on drop. Zero-byte reservations always succeed.
558    ///
559    /// Admission rules, in order:
560    ///
561    /// 1. Under escalation step 1, low-priority classes are rejected
562    ///    ([`MemoryError::LowPriorityRejected`]).
563    /// 2. The request must fit the class budget and the node limit — the
564    ///    configured maximum for reserved classes, the maximum minus the
565    ///    reserved floor for every other class, so replication/control memory
566    ///    is never fully starved (S1E-003 step 5).
567    ///
568    /// The fast path is two atomic adds with exact rollback on rejection; no
569    /// locks, no allocation.
570    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    /// Registers a reclaimable subsystem (held weakly) for the governor to
580    /// drive under escalation step 2. Cold path; not for hot-path use.
581    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    /// Drives registered reclaimable subsystems until at least `budget` bytes
589    /// have been freed (or nothing more is reclaimable), returning the bytes
590    /// actually freed. The entry point of escalation step 2. Cold path.
591    pub fn evict_reclaimable(&self, budget: u64) -> u64 {
592        // Clone the registry so re-entrant registration from a callback
593        // cannot deadlock on the lock; weak refs prune dropped subsystems.
594        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    /// Bytes currently reclaimable from registered subsystems.
610    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    /// Step 3 entry point (S1E-004): while the spill trigger is active, an
619    /// operator holding a [`Reservation`] of a spill-eligible class
620    /// ([`MemoryClass::is_spill_eligible`]) may ask to move up to `bytes` of
621    /// its working set to disk through [`crate::spill::SpillManager`]. On
622    /// success the reservation shrinks immediately — the governor accounts
623    /// the memory as freed — and the returned [`SpillGrant`] records the
624    /// spilled amount; writing the bytes to spill files is charged against
625    /// the query's temporary-disk budget, and reading them back re-reserves
626    /// through [`try_reserve`](Self::try_reserve). Returns `None` when the
627    /// trigger is inactive, the class is not spill-eligible, or the
628    /// reservation holds no bytes.
629    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    /// A point-in-time snapshot of governor state.
651    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    /// Recomputes the escalation level from current pressure with hysteresis:
667    /// escalation is immediate; de-escalation of a level happens only below
668    /// its threshold minus the hysteresis band.
669    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    /// Admission core shared by `try_reserve` and `Reservation::resize`
695    /// growth. Exact accounting: add first, validate the post-add totals,
696    /// roll back on rejection — so a granted set never exceeds its limits.
697    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        // A request larger than the node maximum can never be granted; reject
705        // it before the atomic adds so an absurd size cannot wrap the
706        // counters.
707        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    /// Releases bytes of a class and recomputes escalation (release can
745    /// de-escalate once the hysteresis band is cleared).
746    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/// RAII memory reservation: releases its bytes back to the governor on drop.
760///
761/// Two words plus one `Arc` refcount — no allocation. `Send`/`Sync`, so a
762/// reservation can move across threads and live inside async tasks. Leaking a
763/// reservation (`mem::forget`) leaks its accounting but is memory-safe.
764#[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    /// The class this reservation charged.
773    pub fn class(&self) -> MemoryClass {
774        self.class
775    }
776
777    /// The bytes currently held.
778    pub fn bytes(&self) -> u64 {
779        self.bytes
780    }
781
782    /// Resizes the reservation. Shrinking always succeeds; growth goes
783    /// through the same admission rules as
784    /// [`try_reserve`](MemoryGovernor::try_reserve) and leaves the reservation
785    /// unchanged on failure.
786    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/// Proof that the governor authorized a spill under escalation step 3
821/// (S1E-004): an eligible operator moved `bytes` of working memory out of its
822/// reservation into spill files. A pure token — the memory accounting already
823/// happened in [`MemoryGovernor::request_spill_grant`]; the disk side is
824/// [`crate::spill::SpillManager`]'s per-query budget.
825#[derive(Debug, Clone, Copy, PartialEq, Eq)]
826pub struct SpillGrant {
827    class: MemoryClass,
828    bytes: u64,
829}
830
831impl SpillGrant {
832    /// The memory class the spilled bytes came from.
833    pub fn class(self) -> MemoryClass {
834        self.class
835    }
836
837    /// Bytes the operator spilled (already released from its reservation).
838    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        // S1E-003: exactly these nine pools, in spec order.
854        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        // Explicit clone/drop of the guard still accounts exactly once.
922        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        // Growth past the non-reserved limit (max - floor) fails and leaves
942        // the reservation unchanged.
943        assert!(r.resize(901).is_err());
944        assert_eq!(r.bytes(), 900);
945        assert_eq!(governor.total_used(), 900);
946        // Shrinking always succeeds.
947        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        // Another class is unaffected (bounded by the node total only).
966        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        // Node is at the maximum; zero-byte grants still succeed.
976        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        // The governor is undamaged afterwards.
1001        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        // Adversarial foreground pressure: fill every byte a non-reserved
1008        // class may take (max - floor).
1009        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        // The next non-reserved byte would eat the floor: rejected. At 90%
1018        // pressure escalation step 1 is also active, so low-priority classes
1019        // are rejected as low-priority work; the rest as exhausted.
1020        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        // Replication and network buffers (control plane) still reserve up to
1038        // the full maximum: never fully starved (S1E-003 step 5).
1039        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        // But reserved classes cannot exceed the node maximum either.
1047        assert!(governor.try_reserve(1, MemoryClass::Replication).is_err());
1048        assert!(governor
1049            .try_reserve(1, MemoryClass::NetworkBuffers)
1050            .is_err());
1051        // Releasing foreground pressure re-opens the shared space.
1052        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        // Thresholds 0.70 / 0.80 / 0.90 / 0.95 on a 1000-byte governor (floor
1062        // zero, so the non-reserved cap does not interfere).
1063        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); // 0.60
1075        assert_eq!(governor.escalation(), EscalationLevel::None);
1076        assert!(!governor.should_reject_low_priority_work());
1077
1078        reserve(100, &mut held); // 0.70 — step 1
1079        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); // 0.80 — step 2
1084        assert_eq!(governor.escalation(), EscalationLevel::EvictCaches);
1085        assert!(governor.should_evict_caches());
1086        assert!(!governor.spill_trigger());
1087
1088        reserve(100, &mut held); // 0.90 — step 3 (spill hook fires once)
1089        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); // 0.95 — step 4
1095        assert_eq!(governor.escalation(), EscalationLevel::ThrottleMaintenance);
1096        assert!(governor.should_throttle_maintenance());
1097        assert_eq!(governor.stats().spill_triggers, 1);
1098
1099        // The derived ordering matches the spec's textual order 1 → 4.
1100        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        // Release to just inside the hysteresis band (0.66 > 0.70 - 0.05):
1113        // the level holds.
1114        r.resize(660).unwrap();
1115        assert_eq!(governor.escalation(), EscalationLevel::RejectLowPriority);
1116        // Below the band (0.64 < 0.65): de-escalates.
1117        r.resize(640).unwrap();
1118        assert_eq!(governor.escalation(), EscalationLevel::None);
1119        // Mid-level band: escalate to EvictCaches (0.80), release to 0.76
1120        // (> 0.75) — holds; release to 0.74 — drops to step 1 (0.74 >= 0.70).
1121        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        // No pressure: the trigger is inactive, no grant is issued.
1150        assert!(!governor.spill_trigger());
1151        assert_eq!(governor.request_spill_grant(&mut query, 50), None);
1152        assert_eq!(query.bytes(), 100);
1153
1154        // Drive pressure to step 3 (0.90) with an ineligible class's memory.
1155        let mut cache = governor.try_reserve(800, MemoryClass::PageCache).unwrap();
1156        assert!(governor.spill_trigger());
1157        // Ineligible classes are never granted, even under the trigger.
1158        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        // The eligible query reservation is granted.
1163        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        // The governor accounts the spilled memory as freed immediately — and
1188        // the relief drops the node back below the spill threshold.
1189        assert_eq!(governor.total_used(), 500);
1190        assert!(!governor.spill_trigger());
1191        assert_eq!(governor.request_spill_grant(&mut r, 100), None);
1192
1193        // Fresh pressure re-arms the trigger; requests clamp to held bytes.
1194        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        // An empty reservation earns no grant, even under the trigger.
1203        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        // Reading spilled data back re-reserves through the normal admission
1210        // path (and can be rejected under pressure, like any reservation).
1211        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        // Step 1: new low-priority work rejected even though bytes remain.
1226        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        // Foreground work is still admitted within its limits.
1235        assert!(governor
1236            .try_reserve(100, MemoryClass::QueryExecution)
1237            .is_ok());
1238        drop(_pressure);
1239        // Below the hysteresis band, low-priority work is admitted again.
1240        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        // Step 2: a 100-byte relief frees 60 from the first, 40 from the second.
1273        assert_eq!(governor.evict_reclaimable(100), 100);
1274        assert_eq!(a.reclaimable_bytes(), 0);
1275        assert_eq!(b.reclaimable_bytes(), 60);
1276        // Less reclaimable than requested: frees what exists.
1277        assert_eq!(governor.evict_reclaimable(1000), 60);
1278        // A dropped subsystem is pruned, not driven.
1279        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        // Not strictly increasing in S1E-003 order.
1303        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        // Hysteresis band crossing below zero.
1315        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        // Threshold out of the (0, 1] band.
1324        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        // Single-threaded: granted bytes never exceed max - floor for a
1340        // non-reserved class.
1341        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        // Multi-threaded hammering: accounting returns exactly to zero, and a
1351        // final grant of the full non-reserved limit succeeds afterwards.
1352        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); // rejected
1386        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}