vtcode_core/tools/resilience/
adaptive_rate_limiter.rs1use hashbrown::HashMap;
2use once_cell::sync::Lazy;
3use std::sync::Mutex;
4use std::time::{Duration, Instant};
5use tracing::warn;
6
7struct TokenBucket {
9 capacity: f64,
10 tokens: f64,
11 refill_rate: f64, last_refill: Instant,
13}
14
15impl TokenBucket {
16 fn new(capacity: f64, refill_rate: f64) -> Self {
17 Self {
18 capacity,
19 tokens: capacity,
20 refill_rate,
21 last_refill: Instant::now(),
22 }
23 }
24
25 fn refill(&mut self) {
26 let now = Instant::now();
27 let elapsed = now.duration_since(self.last_refill).as_secs_f64();
28 let new_tokens = elapsed * self.refill_rate;
29
30 if new_tokens > 0.0 {
31 self.tokens = (self.tokens + new_tokens).min(self.capacity);
32 self.last_refill = now;
33 }
34 }
35
36 fn try_acquire(&mut self, cost: f64) -> bool {
37 self.refill();
38 if self.tokens >= cost {
39 self.tokens -= cost;
40 true
41 } else {
42 false
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
49pub enum Priority {
50 Low,
51 Normal,
52 High,
53 Critical,
54}
55
56impl Priority {
57 fn weight(&self) -> f64 {
58 match self {
59 Priority::Low => 2.0,
60 Priority::Normal => 1.0,
61 Priority::High => 0.5,
62 Priority::Critical => 0.1,
63 }
64 }
65}
66
67pub struct AdaptiveRateLimiter {
69 inner: Mutex<RateLimiterInner>,
70 default_capacity: f64,
71 default_refill_rate: f64,
72}
73
74struct RateLimiterInner {
75 buckets: HashMap<String, TokenBucket>,
76 tool_priorities: HashMap<String, Priority>,
77}
78
79impl AdaptiveRateLimiter {
80 pub fn new(default_capacity: f64, default_refill_rate: f64) -> Self {
81 let default_capacity = positive_finite_or(default_capacity, 1.0);
82 let default_refill_rate = positive_finite_or(default_refill_rate, 1.0);
83 Self {
84 inner: Mutex::new(RateLimiterInner {
85 buckets: HashMap::new(),
86 tool_priorities: HashMap::new(),
87 }),
88 default_capacity,
89 default_refill_rate,
90 }
91 }
92}
93
94pub static GLOBAL_ADAPTIVE_RATE_LIMITER: Lazy<AdaptiveRateLimiter> = Lazy::new(AdaptiveRateLimiter::default);
96
97pub fn try_acquire_global(tool_name: &str) -> Result<(), Duration> {
99 GLOBAL_ADAPTIVE_RATE_LIMITER.try_acquire(tool_name)
100}
101
102impl Default for AdaptiveRateLimiter {
103 fn default() -> Self {
104 Self::new(10.0, 2.0)
105 }
106}
107
108impl AdaptiveRateLimiter {
109 pub fn set_priority(&self, tool_name: &str, priority: Priority) {
111 if let Ok(mut inner) = self.inner.lock() {
112 inner.tool_priorities.insert(tool_name.to_string(), priority);
113 } else {
114 warn!("adaptive rate limiter state lock poisoned while setting priority for '{}'", tool_name);
115 }
116 }
117
118 pub fn try_acquire(&self, tool_name: &str) -> Result<(), Duration> {
121 let Ok(mut inner) = self.inner.lock() else {
122 warn!("adaptive rate limiter state lock poisoned while acquiring '{}'", tool_name);
123 return Err(Duration::from_millis(100));
124 };
125
126 let priority = inner.tool_priorities.get(tool_name).copied().unwrap_or(Priority::Normal);
127
128 let bucket = inner
129 .buckets
130 .entry(tool_name.to_owned())
131 .or_insert_with(|| TokenBucket::new(self.default_capacity, self.default_refill_rate));
132 let cost = priority.weight();
133
134 if bucket.try_acquire(cost) {
135 Ok(())
136 } else {
137 let needed = cost - bucket.tokens;
143 let base_wait_secs = needed / bucket.refill_rate;
144
145 let jitter = 1.1;
147
148 let wait_secs = match priority {
149 Priority::Critical => base_wait_secs * 0.5, Priority::High => base_wait_secs * 0.8,
151 Priority::Normal => base_wait_secs * jitter,
152 Priority::Low => base_wait_secs * 1.5 * jitter, };
154
155 Err(Duration::try_from_secs_f64(wait_secs).unwrap_or(Duration::from_secs(60)))
156 }
157 }
158}
159
160fn positive_finite_or(value: f64, fallback: f64) -> f64 {
161 if value.is_finite() && value > 0.0 {
162 value
163 } else {
164 fallback
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::{AdaptiveRateLimiter, Priority};
171 use std::time::Duration;
172
173 #[test]
174 fn returns_wait_hint_when_bucket_exhausted() {
175 let limiter = AdaptiveRateLimiter::new(1.0, 1.0);
176 limiter.try_acquire("tool").unwrap();
177 let wait_hint = limiter
178 .try_acquire("tool")
179 .expect_err("second immediate call should be rate-limited");
180 assert!(wait_hint > Duration::ZERO);
181 }
182
183 #[test]
184 fn high_priority_wait_is_shorter_than_low_priority() {
185 let limiter = AdaptiveRateLimiter::new(0.2, 1.0);
186 limiter.set_priority("high", Priority::High);
187 limiter.set_priority("low", Priority::Low);
188
189 let high_wait = limiter.try_acquire("high").expect_err("high-priority call should be limited");
190 let low_wait = limiter.try_acquire("low").expect_err("low-priority call should be limited");
191
192 assert!(high_wait < low_wait);
193 }
194
195 #[test]
196 fn invalid_default_limits_fall_back_to_positive_values() {
197 let limiter = AdaptiveRateLimiter::new(f64::NAN, 0.0);
198 limiter.try_acquire("tool").unwrap();
199 let wait_hint = limiter
200 .try_acquire("tool")
201 .expect_err("second immediate call should be rate-limited");
202
203 assert!(wait_hint > Duration::ZERO);
204 }
205}