Skip to main content

aurum_core/runtime/
governor.rs

1//! Process/engine resource governor and overload policy (JOE-1596 / JOE-1831).
2//!
3//! Permit waiters use a [`Condvar`] so release wakes waiters promptly instead of
4//! busy-spinning on a short sleep. Cancel/deadline are re-checked on each wake
5//! (and at a short max wait slice so cancel without a release still progresses).
6
7use crate::error::{ProviderError, Result};
8use crate::runtime::op_context::OpContext;
9use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
10use std::sync::{Arc, Condvar, Mutex};
11use std::time::{Duration, Instant};
12
13/// Max time a waiter sleeps before re-checking cancel/deadline when no release
14/// has notified (cancel flags do not currently signal the condvar).
15const WAIT_SLICE: Duration = Duration::from_millis(50);
16
17/// Kind of concurrent work limited by the governor.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum PermitKind {
20    ModelLoad,
21    LocalStt,
22    LocalTts,
23    Remote,
24    Blocking,
25}
26
27/// Configuration for a [`ResourceGovernor`].
28#[derive(Debug, Clone)]
29pub struct GovernorConfig {
30    pub max_model_loads: usize,
31    pub max_local_stt: usize,
32    pub max_local_tts: usize,
33    pub max_remote: usize,
34    pub max_blocking: usize,
35    /// Total Whisper inference threads across concurrent STT jobs.
36    pub max_cpu_threads: usize,
37    /// Soft memory reservation budget (bytes).
38    pub max_memory_bytes: u64,
39    /// Max time to wait for a permit.
40    pub queue_timeout: Duration,
41    /// When true, fail immediately if a permit is not free (no wait).
42    pub fail_fast: bool,
43}
44
45impl Default for GovernorConfig {
46    fn default() -> Self {
47        let cpus = std::thread::available_parallelism()
48            .map(|n| n.get())
49            .unwrap_or(4)
50            .clamp(1, 16);
51        Self {
52            max_model_loads: 2,
53            max_local_stt: 2,
54            max_local_tts: 2,
55            max_remote: 4,
56            max_blocking: 4,
57            max_cpu_threads: cpus,
58            max_memory_bytes: 2 * 1024 * 1024 * 1024, // 2 GiB soft
59            queue_timeout: Duration::from_secs(30),
60            fail_fast: false,
61        }
62    }
63}
64
65impl GovernorConfig {
66    /// Conservative mobile/low-memory profile.
67    pub fn mobile() -> Self {
68        Self {
69            max_model_loads: 1,
70            max_local_stt: 1,
71            max_local_tts: 1,
72            max_remote: 2,
73            max_blocking: 2,
74            max_cpu_threads: 2,
75            max_memory_bytes: 512 * 1024 * 1024,
76            queue_timeout: Duration::from_secs(15),
77            fail_fast: false,
78        }
79    }
80
81    /// Higher-concurrency server profile.
82    pub fn server() -> Self {
83        let cpus = std::thread::available_parallelism()
84            .map(|n| n.get())
85            .unwrap_or(8);
86        Self {
87            max_model_loads: 4,
88            max_local_stt: cpus.max(4),
89            max_local_tts: 4,
90            max_remote: 16,
91            max_blocking: cpus.max(8),
92            max_cpu_threads: cpus,
93            max_memory_bytes: 8 * 1024 * 1024 * 1024,
94            queue_timeout: Duration::from_secs(60),
95            fail_fast: false,
96        }
97    }
98
99    /// Reviewed hard ceiling for CPU-thread budget (JOE-1917).
100    pub const MAX_CPU_THREADS_CEILING: usize = 256;
101    /// Reviewed hard ceiling for concurrent permits of a single kind.
102    pub const MAX_PERMIT_CEILING: usize = 4096;
103
104    /// Validate configuration (reject zero / inverted / above-ceiling budgets).
105    pub fn validate(&self) -> Result<()> {
106        if self.max_model_loads == 0
107            || self.max_local_stt == 0
108            || self.max_local_tts == 0
109            || self.max_remote == 0
110            || self.max_blocking == 0
111            || self.max_cpu_threads == 0
112        {
113            return Err(crate::error::UserError::InvalidConfig {
114                reason: "governor permit and CPU budgets must be >= 1".into(),
115            }
116            .into());
117        }
118        if self.max_cpu_threads > Self::MAX_CPU_THREADS_CEILING {
119            return Err(crate::error::UserError::InvalidConfig {
120                reason: format!(
121                    "max_cpu_threads {} exceeds reviewed ceiling {}",
122                    self.max_cpu_threads,
123                    Self::MAX_CPU_THREADS_CEILING
124                ),
125            }
126            .into());
127        }
128        for (name, v) in [
129            ("max_model_loads", self.max_model_loads),
130            ("max_local_stt", self.max_local_stt),
131            ("max_local_tts", self.max_local_tts),
132            ("max_remote", self.max_remote),
133            ("max_blocking", self.max_blocking),
134        ] {
135            if v > Self::MAX_PERMIT_CEILING {
136                return Err(crate::error::UserError::InvalidConfig {
137                    reason: format!(
138                        "{name} {v} exceeds reviewed ceiling {}",
139                        Self::MAX_PERMIT_CEILING
140                    ),
141                }
142                .into());
143            }
144        }
145        Ok(())
146    }
147}
148
149struct CounterPool {
150    max: usize,
151    in_use: AtomicUsize,
152}
153
154impl CounterPool {
155    fn new(max: usize) -> Self {
156        Self {
157            max: max.max(1),
158            in_use: AtomicUsize::new(0),
159        }
160    }
161
162    fn try_acquire(&self) -> bool {
163        loop {
164            let cur = self.in_use.load(Ordering::SeqCst);
165            if cur >= self.max {
166                return false;
167            }
168            let Some(next) = cur.checked_add(1) else {
169                return false;
170            };
171            if self
172                .in_use
173                .compare_exchange(cur, next, Ordering::SeqCst, Ordering::SeqCst)
174                .is_ok()
175            {
176                return true;
177            }
178        }
179    }
180
181    fn release(&self) {
182        // Saturating: never wrap under zero if a bug double-releases.
183        loop {
184            let cur = self.in_use.load(Ordering::SeqCst);
185            if cur == 0 {
186                debug_assert!(false, "permit released more times than acquired");
187                return;
188            }
189            if self
190                .in_use
191                .compare_exchange(cur, cur - 1, Ordering::SeqCst, Ordering::SeqCst)
192                .is_ok()
193            {
194                return;
195            }
196        }
197    }
198
199    fn in_use(&self) -> usize {
200        self.in_use.load(Ordering::SeqCst)
201    }
202
203    fn max(&self) -> usize {
204        self.max
205    }
206}
207
208/// Process/engine resource governor.
209pub struct ResourceGovernor {
210    config: GovernorConfig,
211    model_loads: CounterPool,
212    local_stt: CounterPool,
213    local_tts: CounterPool,
214    remote: CounterPool,
215    blocking: CounterPool,
216    /// Threads currently allocated to STT jobs.
217    cpu_threads_in_use: AtomicUsize,
218    memory_reserved: AtomicU64,
219    /// Serialize multi-permit acquisition to avoid deadlock across kinds.
220    acquire_lock: Mutex<()>,
221    /// Mutex paired with [`Self::waiters`] for permit queue parking (JOE-1831).
222    wait_mutex: Mutex<()>,
223    /// Signalled on every permit/memory/CPU release so waiters stop spinning.
224    waiters: Condvar,
225}
226
227impl Default for ResourceGovernor {
228    fn default() -> Self {
229        Self::new(GovernorConfig::default())
230    }
231}
232
233impl ResourceGovernor {
234    /// Build a governor after validating `config` (JOE-1917).
235    pub fn try_new(config: GovernorConfig) -> Result<Self> {
236        config.validate()?;
237        Ok(Self {
238            model_loads: CounterPool::new(config.max_model_loads),
239            local_stt: CounterPool::new(config.max_local_stt),
240            local_tts: CounterPool::new(config.max_local_tts),
241            remote: CounterPool::new(config.max_remote),
242            blocking: CounterPool::new(config.max_blocking),
243            cpu_threads_in_use: AtomicUsize::new(0),
244            memory_reserved: AtomicU64::new(0),
245            acquire_lock: Mutex::new(()),
246            wait_mutex: Mutex::new(()),
247            waiters: Condvar::new(),
248            config,
249        })
250    }
251
252    /// Build a governor. Panics if `config` fails validation.
253    /// Prefer [`Self::try_new`] at public SDK boundaries.
254    pub fn new(config: GovernorConfig) -> Self {
255        Self::try_new(config).expect("GovernorConfig::validate failed")
256    }
257
258    /// Wake all permit waiters (called after any resource release).
259    fn notify_waiters(&self) {
260        self.waiters.notify_all();
261    }
262
263    /// Park until a release, cancel, or the wait budget expires (JOE-1831).
264    fn park_wait(&self, ctx: Option<&OpContext>, deadline: Instant) -> Result<()> {
265        if self.config.fail_fast {
266            return Ok(());
267        }
268        let now = Instant::now();
269        if now >= deadline {
270            return Ok(());
271        }
272        let rem = deadline.saturating_duration_since(now).min(WAIT_SLICE);
273        // Prefer OpContext remaining so an absolute deadline is honoured promptly.
274        let slice = ctx
275            .and_then(|c| c.remaining())
276            .map(|r| r.min(rem))
277            .unwrap_or(rem);
278        if slice.is_zero() {
279            return Ok(());
280        }
281        let guard = self.wait_mutex.lock().unwrap_or_else(|e| e.into_inner());
282        let (_g, _timeout) = self
283            .waiters
284            .wait_timeout(guard, slice)
285            .unwrap_or_else(|e| e.into_inner());
286        if let Some(c) = ctx {
287            c.check()?;
288        }
289        Ok(())
290    }
291
292    pub fn config(&self) -> &GovernorConfig {
293        &self.config
294    }
295
296    /// Process-wide default governor (desktop profile).
297    ///
298    /// Isolated engines can construct their own [`ResourceGovernor`] instead.
299    pub fn process_global() -> Arc<Self> {
300        use once_cell::sync::Lazy;
301        static G: Lazy<Arc<ResourceGovernor>> = Lazy::new(|| Arc::new(ResourceGovernor::default()));
302        Arc::clone(&G)
303    }
304
305    fn pool(&self, kind: PermitKind) -> &CounterPool {
306        match kind {
307            PermitKind::ModelLoad => &self.model_loads,
308            PermitKind::LocalStt => &self.local_stt,
309            PermitKind::LocalTts => &self.local_tts,
310            PermitKind::Remote => &self.remote,
311            PermitKind::Blocking => &self.blocking,
312        }
313    }
314
315    /// Acquire a single permit, optionally waiting with cancel/deadline.
316    pub fn acquire(
317        self: &Arc<Self>,
318        kind: PermitKind,
319        ctx: Option<&OpContext>,
320    ) -> Result<ResourcePermit> {
321        let timeout = self.wait_budget(ctx);
322        let deadline = Instant::now() + timeout;
323        loop {
324            if let Some(c) = ctx {
325                c.check()?;
326            }
327            if self.pool(kind).try_acquire() {
328                return Ok(ResourcePermit {
329                    governor: Arc::clone(self),
330                    kind,
331                    cpu_threads: 0,
332                    memory: 0,
333                    holds_blocking: false,
334                    released: false,
335                });
336            }
337            if self.config.fail_fast || Instant::now() >= deadline {
338                return Err(ProviderError::Overload {
339                    reason: format!(
340                        "{kind:?} permits exhausted ({}/{})",
341                        self.pool(kind).in_use(),
342                        self.pool(kind).max()
343                    ),
344                }
345                .into());
346            }
347            self.park_wait(ctx, deadline)?;
348        }
349    }
350
351    fn wait_budget(&self, ctx: Option<&OpContext>) -> Duration {
352        if self.config.fail_fast {
353            return Duration::ZERO;
354        }
355        ctx.and_then(|c| c.remaining())
356            .unwrap_or(self.config.queue_timeout)
357            .min(self.config.queue_timeout)
358    }
359
360    /// Acquire STT permit + blocking permit + CPU thread budget (+ optional memory).
361    ///
362    /// Acquisition order is fixed (memory → STT → blocking → CPU) under a brief
363    /// mutex so concurrent multi-resource acquires cannot deadlock. The mutex is
364    /// not held across sleep/wait.
365    pub fn acquire_stt(
366        self: &Arc<Self>,
367        cpu_threads: usize,
368        memory: u64,
369        ctx: Option<&OpContext>,
370    ) -> Result<ResourcePermit> {
371        let timeout = self.wait_budget(ctx);
372        let deadline = Instant::now() + timeout;
373        let want = cpu_threads.max(1);
374
375        loop {
376            if let Some(c) = ctx {
377                c.check()?;
378            }
379
380            {
381                let _order = self.acquire_lock.lock().unwrap_or_else(|e| e.into_inner());
382
383                // Try memory first.
384                if memory == 0 || self.try_reserve_memory(memory).is_ok() {
385                    if self.local_stt.try_acquire() {
386                        if self.blocking.try_acquire() {
387                            if self.try_reserve_cpu(want) {
388                                return Ok(ResourcePermit {
389                                    governor: Arc::clone(self),
390                                    kind: PermitKind::LocalStt,
391                                    cpu_threads: want,
392                                    memory,
393                                    holds_blocking: true,
394                                    released: false,
395                                });
396                            }
397                            self.blocking.release();
398                        }
399                        self.local_stt.release();
400                    }
401                    self.release_memory(memory);
402                }
403            }
404
405            if self.config.fail_fast || Instant::now() >= deadline {
406                return Err(ProviderError::Overload {
407                    reason: format!(
408                        "STT resources unavailable (cpu want {want}, budget {})",
409                        self.config.max_cpu_threads
410                    ),
411                }
412                .into());
413            }
414            self.park_wait(ctx, deadline)?;
415        }
416    }
417
418    /// Acquire local TTS + blocking permits (+ optional memory).
419    pub fn acquire_tts(
420        self: &Arc<Self>,
421        memory: u64,
422        ctx: Option<&OpContext>,
423    ) -> Result<ResourcePermit> {
424        let timeout = self.wait_budget(ctx);
425        let deadline = Instant::now() + timeout;
426
427        loop {
428            if let Some(c) = ctx {
429                c.check()?;
430            }
431
432            {
433                let _order = self.acquire_lock.lock().unwrap_or_else(|e| e.into_inner());
434
435                if memory == 0 || self.try_reserve_memory(memory).is_ok() {
436                    if self.local_tts.try_acquire() {
437                        if self.blocking.try_acquire() {
438                            return Ok(ResourcePermit {
439                                governor: Arc::clone(self),
440                                kind: PermitKind::LocalTts,
441                                cpu_threads: 0,
442                                memory,
443                                holds_blocking: true,
444                                released: false,
445                            });
446                        }
447                        self.local_tts.release();
448                    }
449                    self.release_memory(memory);
450                }
451            }
452
453            if self.config.fail_fast || Instant::now() >= deadline {
454                return Err(ProviderError::Overload {
455                    reason: format!(
456                        "LocalTts permits exhausted ({}/{})",
457                        self.local_tts.in_use(),
458                        self.local_tts.max()
459                    ),
460                }
461                .into());
462            }
463            self.park_wait(ctx, deadline)?;
464        }
465    }
466
467    fn try_reserve_memory(&self, bytes: u64) -> Result<()> {
468        if bytes == 0 {
469            return Ok(());
470        }
471        loop {
472            let cur = self.memory_reserved.load(Ordering::SeqCst);
473            let new = cur.saturating_add(bytes);
474            if new > self.config.max_memory_bytes {
475                return Err(ProviderError::Overload {
476                    reason: format!(
477                        "memory reservation {bytes} would exceed budget {} (in use {cur})",
478                        self.config.max_memory_bytes
479                    ),
480                }
481                .into());
482            }
483            if self
484                .memory_reserved
485                .compare_exchange(cur, new, Ordering::SeqCst, Ordering::SeqCst)
486                .is_ok()
487            {
488                return Ok(());
489            }
490        }
491    }
492
493    fn release_memory(&self, bytes: u64) {
494        if bytes > 0 {
495            self.memory_reserved.fetch_sub(bytes, Ordering::SeqCst);
496            self.notify_waiters();
497        }
498    }
499
500    fn try_reserve_cpu(&self, n: usize) -> bool {
501        if n == 0 {
502            return true;
503        }
504        // Cap requested threads to the configured budget so usize::MAX cannot wrap
505        // past the check (JOE-1917).
506        if n > self.config.max_cpu_threads {
507            return false;
508        }
509        loop {
510            let cur = self.cpu_threads_in_use.load(Ordering::SeqCst);
511            let Some(next) = cur.checked_add(n) else {
512                return false;
513            };
514            if next > self.config.max_cpu_threads {
515                return false;
516            }
517            if self
518                .cpu_threads_in_use
519                .compare_exchange(cur, next, Ordering::SeqCst, Ordering::SeqCst)
520                .is_ok()
521            {
522                return true;
523            }
524        }
525    }
526
527    fn release_cpu(&self, n: usize) {
528        if n == 0 {
529            return;
530        }
531        // Saturating subtract so a bug cannot wrap under zero and inflate budget.
532        loop {
533            let cur = self.cpu_threads_in_use.load(Ordering::SeqCst);
534            let next = cur.saturating_sub(n);
535            if self
536                .cpu_threads_in_use
537                .compare_exchange(cur, next, Ordering::SeqCst, Ordering::SeqCst)
538                .is_ok()
539            {
540                break;
541            }
542        }
543        self.notify_waiters();
544    }
545
546    /// How many Whisper threads a new STT job should use given remaining budget.
547    pub fn recommend_stt_threads(&self) -> usize {
548        let used = self.cpu_threads_in_use.load(Ordering::SeqCst);
549        let rem = self.config.max_cpu_threads.saturating_sub(used).max(1);
550        // Fair share: leave room for concurrent jobs when budget allows.
551        let fair = (self.config.max_cpu_threads / self.config.max_local_stt.max(1)).max(1);
552        fair.min(rem).clamp(1, 8)
553    }
554
555    pub fn stats(&self) -> GovernorStats {
556        GovernorStats {
557            model_loads: self.model_loads.in_use(),
558            local_stt: self.local_stt.in_use(),
559            local_tts: self.local_tts.in_use(),
560            remote: self.remote.in_use(),
561            blocking: self.blocking.in_use(),
562            cpu_threads: self.cpu_threads_in_use.load(Ordering::SeqCst),
563            memory_reserved: self.memory_reserved.load(Ordering::SeqCst),
564            max_cpu_threads: self.config.max_cpu_threads,
565            max_memory_bytes: self.config.max_memory_bytes,
566        }
567    }
568}
569
570/// Snapshot of governor occupancy.
571#[derive(Debug, Clone)]
572pub struct GovernorStats {
573    pub model_loads: usize,
574    pub local_stt: usize,
575    pub local_tts: usize,
576    pub remote: usize,
577    pub blocking: usize,
578    pub cpu_threads: usize,
579    pub memory_reserved: u64,
580    pub max_cpu_threads: usize,
581    pub max_memory_bytes: u64,
582}
583
584/// RAII permit — releases all held resources on drop.
585///
586/// Holds an [`Arc`] so permits can cross `.await` points.
587pub struct ResourcePermit {
588    governor: Arc<ResourceGovernor>,
589    kind: PermitKind,
590    cpu_threads: usize,
591    memory: u64,
592    holds_blocking: bool,
593    released: bool,
594}
595
596impl std::fmt::Debug for ResourcePermit {
597    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
598        f.debug_struct("ResourcePermit")
599            .field("kind", &self.kind)
600            .field("cpu_threads", &self.cpu_threads)
601            .field("memory", &self.memory)
602            .field("holds_blocking", &self.holds_blocking)
603            .finish()
604    }
605}
606
607impl ResourcePermit {
608    pub fn kind(&self) -> PermitKind {
609        self.kind
610    }
611
612    pub fn cpu_threads(&self) -> usize {
613        self.cpu_threads
614    }
615
616    fn release_inner(&mut self) {
617        if self.released {
618            return;
619        }
620        self.released = true;
621        if self.cpu_threads > 0 {
622            self.governor.release_cpu(self.cpu_threads);
623            self.cpu_threads = 0;
624        }
625        if self.holds_blocking {
626            self.governor.blocking.release();
627            self.holds_blocking = false;
628        }
629        if self.memory > 0 {
630            self.governor.release_memory(self.memory);
631            self.memory = 0;
632        }
633        self.governor.pool(self.kind).release();
634        // Always wake waiters once per full permit release (CPU/memory already
635        // notified when non-zero; a second notify_all is cheap and covers the
636        // pure-permit case where only the kind counter changed).
637        self.governor.notify_waiters();
638    }
639}
640
641impl Drop for ResourcePermit {
642    fn drop(&mut self) {
643        self.release_inner();
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650
651    #[test]
652    fn permits_cap() {
653        let g = Arc::new(ResourceGovernor::new(GovernorConfig {
654            max_local_stt: 1,
655            fail_fast: true,
656            ..GovernorConfig::default()
657        }));
658        let a = g.acquire(PermitKind::LocalStt, None).unwrap();
659        let err = g.acquire(PermitKind::LocalStt, None).unwrap_err();
660        assert!(matches!(
661            err,
662            crate::error::TranscriptionError::Provider(ProviderError::Overload { .. })
663        ));
664        drop(a);
665        let _b = g.acquire(PermitKind::LocalStt, None).unwrap();
666    }
667
668    #[test]
669    fn memory_budget() {
670        let g = Arc::new(ResourceGovernor::new(GovernorConfig {
671            max_memory_bytes: 1000,
672            fail_fast: true,
673            ..GovernorConfig::default()
674        }));
675        assert!(g.try_reserve_memory(600).is_ok());
676        assert!(g.try_reserve_memory(600).is_err());
677        g.release_memory(600);
678        assert!(g.try_reserve_memory(600).is_ok());
679    }
680
681    #[test]
682    fn cpu_budget() {
683        let g = Arc::new(ResourceGovernor::new(GovernorConfig {
684            max_cpu_threads: 4,
685            max_local_stt: 4,
686            max_blocking: 4,
687            fail_fast: true,
688            ..GovernorConfig::default()
689        }));
690        let p = g.acquire_stt(3, 0, None).unwrap();
691        assert_eq!(p.cpu_threads(), 3);
692        let err = g.acquire_stt(3, 0, None).unwrap_err();
693        assert!(err.to_string().contains("CPU") || err.to_string().contains("overload"));
694        drop(p);
695        let _p2 = g.acquire_stt(2, 0, None).unwrap();
696    }
697
698    #[test]
699    fn tts_releases_blocking() {
700        let g = Arc::new(ResourceGovernor::new(GovernorConfig {
701            max_local_tts: 1,
702            max_blocking: 1,
703            fail_fast: true,
704            ..GovernorConfig::default()
705        }));
706        let p = g.acquire_tts(0, None).unwrap();
707        let err = g.acquire_tts(0, None).unwrap_err();
708        assert!(matches!(
709            err,
710            crate::error::TranscriptionError::Provider(ProviderError::Overload { .. })
711        ));
712        drop(p);
713        let _p2 = g.acquire_tts(0, None).unwrap();
714    }
715
716    #[test]
717    fn cancel_aborts_queue_wait() {
718        let g = Arc::new(ResourceGovernor::new(GovernorConfig {
719            max_local_stt: 1,
720            fail_fast: false,
721            queue_timeout: Duration::from_secs(5),
722            ..GovernorConfig::default()
723        }));
724        let _hold = g.acquire(PermitKind::LocalStt, None).unwrap();
725        let ctx = OpContext::new();
726        ctx.cancel.cancel();
727        let err = g.acquire(PermitKind::LocalStt, Some(&ctx)).unwrap_err();
728        assert!(matches!(
729            err,
730            crate::error::TranscriptionError::Provider(ProviderError::Cancelled)
731        ));
732    }
733
734    #[test]
735    fn config_validate_rejects_zero() {
736        let c = GovernorConfig {
737            max_cpu_threads: 0,
738            ..GovernorConfig::default()
739        };
740        assert!(c.validate().is_err());
741    }
742
743    #[test]
744    fn try_new_rejects_invalid_and_accepts_default() {
745        assert!(ResourceGovernor::try_new(GovernorConfig::default()).is_ok());
746        let bad = GovernorConfig {
747            max_cpu_threads: 0,
748            ..GovernorConfig::default()
749        };
750        assert!(ResourceGovernor::try_new(bad).is_err());
751        let over = GovernorConfig {
752            max_cpu_threads: GovernorConfig::MAX_CPU_THREADS_CEILING + 1,
753            ..GovernorConfig::default()
754        };
755        assert!(ResourceGovernor::try_new(over).is_err());
756    }
757
758    #[test]
759    fn cpu_reserve_rejects_usize_max_without_wrap() {
760        let g = ResourceGovernor::new(GovernorConfig {
761            max_cpu_threads: 4,
762            max_local_stt: 1,
763            ..GovernorConfig::default()
764        });
765        assert!(!g.try_reserve_cpu(usize::MAX));
766        assert!(!g.try_reserve_cpu(5));
767        assert!(g.try_reserve_cpu(3));
768        assert!(!g.try_reserve_cpu(2)); // 3+2 > 4
769        g.release_cpu(3);
770        assert!(g.try_reserve_cpu(4));
771        g.release_cpu(4);
772        // double-release must not wrap under zero into huge free budget
773        g.release_cpu(4);
774        assert_eq!(g.stats().cpu_threads, 0);
775    }
776}