do_memory_core/retry/
budget.rs1use std::sync::Arc;
2use std::sync::atomic::{AtomicU32, Ordering};
3use std::time::{Duration, Instant};
4
5use parking_lot::Mutex;
6
7pub struct RetryBudget {
13 max_tokens: u32,
14 window: Duration,
15 tokens: AtomicU32,
16 window_start: Mutex<Instant>,
17}
18
19impl RetryBudget {
20 pub fn new(max_tokens: u32, window: Duration) -> Arc<Self> {
25 Arc::new(Self {
26 max_tokens,
27 window,
28 tokens: AtomicU32::new(max_tokens),
29 window_start: Mutex::new(Instant::now()),
30 })
31 }
32
33 pub fn acquire(&self) -> bool {
38 self.maybe_refill();
39 let prev = self.tokens.fetch_sub(1, Ordering::SeqCst);
40 if prev > 0 {
41 true
42 } else {
43 self.tokens.fetch_add(1, Ordering::SeqCst);
45 false
46 }
47 }
48
49 pub fn available(&self) -> u32 {
51 self.maybe_refill();
52 self.tokens.load(Ordering::SeqCst)
53 }
54
55 fn maybe_refill(&self) {
56 let mut start = self.window_start.lock();
57 let elapsed = start.elapsed();
58 if elapsed >= self.window {
59 self.tokens.store(self.max_tokens, Ordering::SeqCst);
60 *start = Instant::now();
61 }
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn token_exhaustion() {
71 let budget = RetryBudget::new(2, Duration::from_secs(10));
72 assert!(budget.acquire());
73 assert!(budget.acquire());
74 assert!(!budget.acquire());
75 assert_eq!(budget.available(), 0);
76 }
77
78 #[test]
79 fn window_refill() {
80 let budget = RetryBudget::new(1, Duration::from_millis(50));
81 assert!(budget.acquire());
82 assert!(!budget.acquire());
83
84 std::thread::sleep(Duration::from_millis(60));
85 assert!(budget.acquire());
86 assert!(!budget.acquire());
87 }
88
89 #[test]
90 fn concurrent_acquisition() {
91 use std::sync::Arc;
92 use std::thread;
93
94 let budget = RetryBudget::new(5, Duration::from_secs(10));
95 let budget = Arc::new(budget);
96
97 let handles: Vec<_> = (0..10)
98 .map(|_| {
99 let b = Arc::clone(&budget);
100 thread::spawn(move || b.acquire())
101 })
102 .collect();
103
104 let mut acquired = 0;
105 for h in handles {
106 if h.join().unwrap() {
107 acquired += 1;
108 }
109 }
110 assert_eq!(acquired, 5);
111 assert_eq!(budget.available(), 0);
112 }
113}