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 fn validate(&self) -> Result<()> {
101 if self.max_model_loads == 0
102 || self.max_local_stt == 0
103 || self.max_local_tts == 0
104 || self.max_remote == 0
105 || self.max_blocking == 0
106 || self.max_cpu_threads == 0
107 {
108 return Err(crate::error::UserError::InvalidConfig {
109 reason: "governor permit and CPU budgets must be >= 1".into(),
110 }
111 .into());
112 }
113 Ok(())
114 }
115}
116
117struct CounterPool {
118 max: usize,
119 in_use: AtomicUsize,
120}
121
122impl CounterPool {
123 fn new(max: usize) -> Self {
124 Self {
125 max: max.max(1),
126 in_use: AtomicUsize::new(0),
127 }
128 }
129
130 fn try_acquire(&self) -> bool {
131 loop {
132 let cur = self.in_use.load(Ordering::SeqCst);
133 if cur >= self.max {
134 return false;
135 }
136 if self
137 .in_use
138 .compare_exchange(cur, cur + 1, Ordering::SeqCst, Ordering::SeqCst)
139 .is_ok()
140 {
141 return true;
142 }
143 }
144 }
145
146 fn release(&self) {
147 let prev = self.in_use.fetch_sub(1, Ordering::SeqCst);
148 debug_assert!(prev > 0, "permit released more times than acquired");
149 }
150
151 fn in_use(&self) -> usize {
152 self.in_use.load(Ordering::SeqCst)
153 }
154
155 fn max(&self) -> usize {
156 self.max
157 }
158}
159
160pub struct ResourceGovernor {
162 config: GovernorConfig,
163 model_loads: CounterPool,
164 local_stt: CounterPool,
165 local_tts: CounterPool,
166 remote: CounterPool,
167 blocking: CounterPool,
168 cpu_threads_in_use: AtomicUsize,
170 memory_reserved: AtomicU64,
171 acquire_lock: Mutex<()>,
173 wait_mutex: Mutex<()>,
175 waiters: Condvar,
177}
178
179impl Default for ResourceGovernor {
180 fn default() -> Self {
181 Self::new(GovernorConfig::default())
182 }
183}
184
185impl ResourceGovernor {
186 pub fn new(config: GovernorConfig) -> Self {
187 Self {
188 model_loads: CounterPool::new(config.max_model_loads),
189 local_stt: CounterPool::new(config.max_local_stt),
190 local_tts: CounterPool::new(config.max_local_tts),
191 remote: CounterPool::new(config.max_remote),
192 blocking: CounterPool::new(config.max_blocking),
193 cpu_threads_in_use: AtomicUsize::new(0),
194 memory_reserved: AtomicU64::new(0),
195 acquire_lock: Mutex::new(()),
196 wait_mutex: Mutex::new(()),
197 waiters: Condvar::new(),
198 config,
199 }
200 }
201
202 fn notify_waiters(&self) {
204 self.waiters.notify_all();
205 }
206
207 fn park_wait(&self, ctx: Option<&OpContext>, deadline: Instant) -> Result<()> {
209 if self.config.fail_fast {
210 return Ok(());
211 }
212 let now = Instant::now();
213 if now >= deadline {
214 return Ok(());
215 }
216 let rem = deadline.saturating_duration_since(now).min(WAIT_SLICE);
217 let slice = ctx
219 .and_then(|c| c.remaining())
220 .map(|r| r.min(rem))
221 .unwrap_or(rem);
222 if slice.is_zero() {
223 return Ok(());
224 }
225 let guard = self.wait_mutex.lock().unwrap_or_else(|e| e.into_inner());
226 let (_g, _timeout) = self
227 .waiters
228 .wait_timeout(guard, slice)
229 .unwrap_or_else(|e| e.into_inner());
230 if let Some(c) = ctx {
231 c.check()?;
232 }
233 Ok(())
234 }
235
236 pub fn config(&self) -> &GovernorConfig {
237 &self.config
238 }
239
240 pub fn process_global() -> Arc<Self> {
244 use once_cell::sync::Lazy;
245 static G: Lazy<Arc<ResourceGovernor>> = Lazy::new(|| Arc::new(ResourceGovernor::default()));
246 Arc::clone(&G)
247 }
248
249 fn pool(&self, kind: PermitKind) -> &CounterPool {
250 match kind {
251 PermitKind::ModelLoad => &self.model_loads,
252 PermitKind::LocalStt => &self.local_stt,
253 PermitKind::LocalTts => &self.local_tts,
254 PermitKind::Remote => &self.remote,
255 PermitKind::Blocking => &self.blocking,
256 }
257 }
258
259 pub fn acquire(
261 self: &Arc<Self>,
262 kind: PermitKind,
263 ctx: Option<&OpContext>,
264 ) -> Result<ResourcePermit> {
265 let timeout = self.wait_budget(ctx);
266 let deadline = Instant::now() + timeout;
267 loop {
268 if let Some(c) = ctx {
269 c.check()?;
270 }
271 if self.pool(kind).try_acquire() {
272 return Ok(ResourcePermit {
273 governor: Arc::clone(self),
274 kind,
275 cpu_threads: 0,
276 memory: 0,
277 holds_blocking: false,
278 released: false,
279 });
280 }
281 if self.config.fail_fast || Instant::now() >= deadline {
282 return Err(ProviderError::Overload {
283 reason: format!(
284 "{kind:?} permits exhausted ({}/{})",
285 self.pool(kind).in_use(),
286 self.pool(kind).max()
287 ),
288 }
289 .into());
290 }
291 self.park_wait(ctx, deadline)?;
292 }
293 }
294
295 fn wait_budget(&self, ctx: Option<&OpContext>) -> Duration {
296 if self.config.fail_fast {
297 return Duration::ZERO;
298 }
299 ctx.and_then(|c| c.remaining())
300 .unwrap_or(self.config.queue_timeout)
301 .min(self.config.queue_timeout)
302 }
303
304 pub fn acquire_stt(
310 self: &Arc<Self>,
311 cpu_threads: usize,
312 memory: u64,
313 ctx: Option<&OpContext>,
314 ) -> Result<ResourcePermit> {
315 let timeout = self.wait_budget(ctx);
316 let deadline = Instant::now() + timeout;
317 let want = cpu_threads.max(1);
318
319 loop {
320 if let Some(c) = ctx {
321 c.check()?;
322 }
323
324 {
325 let _order = self.acquire_lock.lock().unwrap_or_else(|e| e.into_inner());
326
327 if memory == 0 || self.try_reserve_memory(memory).is_ok() {
329 if self.local_stt.try_acquire() {
330 if self.blocking.try_acquire() {
331 if self.try_reserve_cpu(want) {
332 return Ok(ResourcePermit {
333 governor: Arc::clone(self),
334 kind: PermitKind::LocalStt,
335 cpu_threads: want,
336 memory,
337 holds_blocking: true,
338 released: false,
339 });
340 }
341 self.blocking.release();
342 }
343 self.local_stt.release();
344 }
345 self.release_memory(memory);
346 }
347 }
348
349 if self.config.fail_fast || Instant::now() >= deadline {
350 return Err(ProviderError::Overload {
351 reason: format!(
352 "STT resources unavailable (cpu want {want}, budget {})",
353 self.config.max_cpu_threads
354 ),
355 }
356 .into());
357 }
358 self.park_wait(ctx, deadline)?;
359 }
360 }
361
362 pub fn acquire_tts(
364 self: &Arc<Self>,
365 memory: u64,
366 ctx: Option<&OpContext>,
367 ) -> Result<ResourcePermit> {
368 let timeout = self.wait_budget(ctx);
369 let deadline = Instant::now() + timeout;
370
371 loop {
372 if let Some(c) = ctx {
373 c.check()?;
374 }
375
376 {
377 let _order = self.acquire_lock.lock().unwrap_or_else(|e| e.into_inner());
378
379 if memory == 0 || self.try_reserve_memory(memory).is_ok() {
380 if self.local_tts.try_acquire() {
381 if self.blocking.try_acquire() {
382 return Ok(ResourcePermit {
383 governor: Arc::clone(self),
384 kind: PermitKind::LocalTts,
385 cpu_threads: 0,
386 memory,
387 holds_blocking: true,
388 released: false,
389 });
390 }
391 self.local_tts.release();
392 }
393 self.release_memory(memory);
394 }
395 }
396
397 if self.config.fail_fast || Instant::now() >= deadline {
398 return Err(ProviderError::Overload {
399 reason: format!(
400 "LocalTts permits exhausted ({}/{})",
401 self.local_tts.in_use(),
402 self.local_tts.max()
403 ),
404 }
405 .into());
406 }
407 self.park_wait(ctx, deadline)?;
408 }
409 }
410
411 fn try_reserve_memory(&self, bytes: u64) -> Result<()> {
412 if bytes == 0 {
413 return Ok(());
414 }
415 loop {
416 let cur = self.memory_reserved.load(Ordering::SeqCst);
417 let new = cur.saturating_add(bytes);
418 if new > self.config.max_memory_bytes {
419 return Err(ProviderError::Overload {
420 reason: format!(
421 "memory reservation {bytes} would exceed budget {} (in use {cur})",
422 self.config.max_memory_bytes
423 ),
424 }
425 .into());
426 }
427 if self
428 .memory_reserved
429 .compare_exchange(cur, new, Ordering::SeqCst, Ordering::SeqCst)
430 .is_ok()
431 {
432 return Ok(());
433 }
434 }
435 }
436
437 fn release_memory(&self, bytes: u64) {
438 if bytes > 0 {
439 self.memory_reserved.fetch_sub(bytes, Ordering::SeqCst);
440 self.notify_waiters();
441 }
442 }
443
444 fn try_reserve_cpu(&self, n: usize) -> bool {
445 loop {
446 let cur = self.cpu_threads_in_use.load(Ordering::SeqCst);
447 if cur + n > self.config.max_cpu_threads {
448 return false;
449 }
450 if self
451 .cpu_threads_in_use
452 .compare_exchange(cur, cur + n, Ordering::SeqCst, Ordering::SeqCst)
453 .is_ok()
454 {
455 return true;
456 }
457 }
458 }
459
460 fn release_cpu(&self, n: usize) {
461 if n > 0 {
462 self.cpu_threads_in_use.fetch_sub(n, Ordering::SeqCst);
463 self.notify_waiters();
464 }
465 }
466
467 pub fn recommend_stt_threads(&self) -> usize {
469 let used = self.cpu_threads_in_use.load(Ordering::SeqCst);
470 let rem = self.config.max_cpu_threads.saturating_sub(used).max(1);
471 let fair = (self.config.max_cpu_threads / self.config.max_local_stt.max(1)).max(1);
473 fair.min(rem).clamp(1, 8)
474 }
475
476 pub fn stats(&self) -> GovernorStats {
477 GovernorStats {
478 model_loads: self.model_loads.in_use(),
479 local_stt: self.local_stt.in_use(),
480 local_tts: self.local_tts.in_use(),
481 remote: self.remote.in_use(),
482 blocking: self.blocking.in_use(),
483 cpu_threads: self.cpu_threads_in_use.load(Ordering::SeqCst),
484 memory_reserved: self.memory_reserved.load(Ordering::SeqCst),
485 max_cpu_threads: self.config.max_cpu_threads,
486 max_memory_bytes: self.config.max_memory_bytes,
487 }
488 }
489}
490
491#[derive(Debug, Clone)]
493pub struct GovernorStats {
494 pub model_loads: usize,
495 pub local_stt: usize,
496 pub local_tts: usize,
497 pub remote: usize,
498 pub blocking: usize,
499 pub cpu_threads: usize,
500 pub memory_reserved: u64,
501 pub max_cpu_threads: usize,
502 pub max_memory_bytes: u64,
503}
504
505pub struct ResourcePermit {
509 governor: Arc<ResourceGovernor>,
510 kind: PermitKind,
511 cpu_threads: usize,
512 memory: u64,
513 holds_blocking: bool,
514 released: bool,
515}
516
517impl std::fmt::Debug for ResourcePermit {
518 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519 f.debug_struct("ResourcePermit")
520 .field("kind", &self.kind)
521 .field("cpu_threads", &self.cpu_threads)
522 .field("memory", &self.memory)
523 .field("holds_blocking", &self.holds_blocking)
524 .finish()
525 }
526}
527
528impl ResourcePermit {
529 pub fn kind(&self) -> PermitKind {
530 self.kind
531 }
532
533 pub fn cpu_threads(&self) -> usize {
534 self.cpu_threads
535 }
536
537 fn release_inner(&mut self) {
538 if self.released {
539 return;
540 }
541 self.released = true;
542 if self.cpu_threads > 0 {
543 self.governor.release_cpu(self.cpu_threads);
544 self.cpu_threads = 0;
545 }
546 if self.holds_blocking {
547 self.governor.blocking.release();
548 self.holds_blocking = false;
549 }
550 if self.memory > 0 {
551 self.governor.release_memory(self.memory);
552 self.memory = 0;
553 }
554 self.governor.pool(self.kind).release();
555 self.governor.notify_waiters();
559 }
560}
561
562impl Drop for ResourcePermit {
563 fn drop(&mut self) {
564 self.release_inner();
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
573 fn permits_cap() {
574 let g = Arc::new(ResourceGovernor::new(GovernorConfig {
575 max_local_stt: 1,
576 fail_fast: true,
577 ..GovernorConfig::default()
578 }));
579 let a = g.acquire(PermitKind::LocalStt, None).unwrap();
580 let err = g.acquire(PermitKind::LocalStt, None).unwrap_err();
581 assert!(matches!(
582 err,
583 crate::error::TranscriptionError::Provider(ProviderError::Overload { .. })
584 ));
585 drop(a);
586 let _b = g.acquire(PermitKind::LocalStt, None).unwrap();
587 }
588
589 #[test]
590 fn memory_budget() {
591 let g = Arc::new(ResourceGovernor::new(GovernorConfig {
592 max_memory_bytes: 1000,
593 fail_fast: true,
594 ..GovernorConfig::default()
595 }));
596 assert!(g.try_reserve_memory(600).is_ok());
597 assert!(g.try_reserve_memory(600).is_err());
598 g.release_memory(600);
599 assert!(g.try_reserve_memory(600).is_ok());
600 }
601
602 #[test]
603 fn cpu_budget() {
604 let g = Arc::new(ResourceGovernor::new(GovernorConfig {
605 max_cpu_threads: 4,
606 max_local_stt: 4,
607 max_blocking: 4,
608 fail_fast: true,
609 ..GovernorConfig::default()
610 }));
611 let p = g.acquire_stt(3, 0, None).unwrap();
612 assert_eq!(p.cpu_threads(), 3);
613 let err = g.acquire_stt(3, 0, None).unwrap_err();
614 assert!(err.to_string().contains("CPU") || err.to_string().contains("overload"));
615 drop(p);
616 let _p2 = g.acquire_stt(2, 0, None).unwrap();
617 }
618
619 #[test]
620 fn tts_releases_blocking() {
621 let g = Arc::new(ResourceGovernor::new(GovernorConfig {
622 max_local_tts: 1,
623 max_blocking: 1,
624 fail_fast: true,
625 ..GovernorConfig::default()
626 }));
627 let p = g.acquire_tts(0, None).unwrap();
628 let err = g.acquire_tts(0, None).unwrap_err();
629 assert!(matches!(
630 err,
631 crate::error::TranscriptionError::Provider(ProviderError::Overload { .. })
632 ));
633 drop(p);
634 let _p2 = g.acquire_tts(0, None).unwrap();
635 }
636
637 #[test]
638 fn cancel_aborts_queue_wait() {
639 let g = Arc::new(ResourceGovernor::new(GovernorConfig {
640 max_local_stt: 1,
641 fail_fast: false,
642 queue_timeout: Duration::from_secs(5),
643 ..GovernorConfig::default()
644 }));
645 let _hold = g.acquire(PermitKind::LocalStt, None).unwrap();
646 let ctx = OpContext::new();
647 ctx.cancel.cancel();
648 let err = g.acquire(PermitKind::LocalStt, Some(&ctx)).unwrap_err();
649 assert!(matches!(
650 err,
651 crate::error::TranscriptionError::Provider(ProviderError::Cancelled)
652 ));
653 }
654
655 #[test]
656 fn config_validate_rejects_zero() {
657 let c = GovernorConfig {
658 max_cpu_threads: 0,
659 ..GovernorConfig::default()
660 };
661 assert!(c.validate().is_err());
662 }
663}