1use 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
13const WAIT_SLICE: Duration = Duration::from_millis(50);
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum PermitKind {
20 ModelLoad,
21 LocalStt,
22 LocalTts,
23 Remote,
24 Blocking,
25}
26
27#[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 pub max_cpu_threads: usize,
37 pub max_memory_bytes: u64,
39 pub queue_timeout: Duration,
41 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, queue_timeout: Duration::from_secs(30),
60 fail_fast: false,
61 }
62 }
63}
64
65impl GovernorConfig {
66 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 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 pub const MAX_CPU_THREADS_CEILING: usize = 256;
101 pub const MAX_PERMIT_CEILING: usize = 4096;
103
104 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 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
208pub struct ResourceGovernor {
210 config: GovernorConfig,
211 model_loads: CounterPool,
212 local_stt: CounterPool,
213 local_tts: CounterPool,
214 remote: CounterPool,
215 blocking: CounterPool,
216 cpu_threads_in_use: AtomicUsize,
218 memory_reserved: AtomicU64,
219 acquire_lock: Mutex<()>,
221 wait_mutex: Mutex<()>,
223 waiters: Condvar,
225}
226
227impl Default for ResourceGovernor {
228 fn default() -> Self {
229 Self::new(GovernorConfig::default())
230 }
231}
232
233impl ResourceGovernor {
234 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 pub fn new(config: GovernorConfig) -> Self {
255 Self::try_new(config).expect("GovernorConfig::validate failed")
256 }
257
258 fn notify_waiters(&self) {
260 self.waiters.notify_all();
261 }
262
263 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 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 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 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 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 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 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 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 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 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 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#[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
584pub 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 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)); g.release_cpu(3);
770 assert!(g.try_reserve_cpu(4));
771 g.release_cpu(4);
772 g.release_cpu(4);
774 assert_eq!(g.stats().cpu_threads, 0);
775 }
776}