vtcode_core/tools/resilience/
rate_limiter.rs1use anyhow::{Result, anyhow};
39use std::sync::{Mutex, MutexGuard};
40use std::time::Instant;
41
42#[derive(Debug, Clone, Copy)]
44pub struct RateLimiterConfig {
45 pub per_sec: u32,
47 pub burst: u32,
49}
50
51impl Default for RateLimiterConfig {
52 fn default() -> Self {
53 let per_sec = std::env::var("VTTOOL_RATE_LIMIT")
55 .ok()
56 .and_then(|v| v.parse().ok())
57 .unwrap_or(20);
58 let burst = std::env::var("VTTOOL_BURST")
59 .ok()
60 .and_then(|v| v.parse().ok())
61 .unwrap_or(per_sec.max(5));
62 RateLimiterConfig { per_sec, burst }
63 }
64}
65
66pub struct RateLimiterInner {
68 config: RateLimiterConfig,
69 tokens: u32,
71 last_refill: Instant,
73}
74
75impl RateLimiterInner {
76 fn new() -> Self {
77 Self::new_with_config(RateLimiterConfig::default())
78 }
79
80 pub fn new_with_config(config: RateLimiterConfig) -> Self {
81 Self {
82 config,
83 tokens: config.burst,
84 last_refill: Instant::now(),
85 }
86 }
87
88 fn refill(&mut self, speed_multiplier: f64) {
92 let now = Instant::now();
93 let elapsed = now.duration_since(self.last_refill);
94
95 let millis = elapsed.as_millis() as u64;
97
98 if millis < 50 {
100 return;
101 }
102
103 let effective_rate = (self.config.per_sec as f64 * speed_multiplier) as u64;
105
106 let added = u32::try_from(effective_rate.saturating_mul(millis) / 1000).unwrap_or(u32::MAX);
108
109 if added > 0 {
110 let effective_burst = self.config.burst as f64 * speed_multiplier.max(1.0);
111 let effective_burst: u32 = if effective_burst.is_finite()
112 && effective_burst >= 0.0
113 && effective_burst <= u32::MAX as f64
114 {
115 effective_burst as u32
116 } else {
117 u32::MAX
118 };
119 self.tokens = self.tokens.saturating_add(added).min(effective_burst);
120 self.last_refill = now;
121 }
122 }
123
124 pub fn try_acquire(&mut self) -> Result<()> {
126 self.try_acquire_scaled(1.0)
127 }
128
129 pub fn try_acquire_scaled(&mut self, speed_multiplier: f64) -> Result<()> {
131 self.refill(speed_multiplier);
132 if self.tokens == 0 {
133 Err(anyhow!("tool rate limit exceeded"))
134 } else {
135 self.tokens -= 1;
136 Ok(())
137 }
138 }
139}
140
141pub type RateLimiter = PerToolRateLimiter;
143
144use crate::types::CompactStr;
145use hashbrown::HashMap;
146use once_cell::sync::Lazy;
152
153pub static GLOBAL_RATE_LIMITER: Lazy<Mutex<RateLimiterInner>> =
154 Lazy::new(|| Mutex::new(RateLimiterInner::new()));
155
156pub struct PerToolRateLimiter {
159 buckets: HashMap<CompactStr, RateLimiterInner>,
161 default_config: RateLimiterConfig,
163}
164
165impl Default for PerToolRateLimiter {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171impl PerToolRateLimiter {
172 pub fn new() -> Self {
174 Self {
175 buckets: HashMap::new(),
176 default_config: RateLimiterConfig::default(),
177 }
178 }
179
180 pub fn new_with_config(config: RateLimiterConfig) -> Self {
181 Self {
182 buckets: HashMap::new(),
183 default_config: config,
184 }
185 }
186
187 pub fn try_acquire_for(&mut self, tool_name: &str) -> Result<()> {
190 self.try_acquire_for_scaled(tool_name, 1.0)
191 }
192
193 pub fn try_acquire_for_scaled(&mut self, tool_name: &str, multiplier: f64) -> Result<()> {
195 let bucket = self
196 .buckets
197 .entry(CompactStr::from(tool_name))
198 .or_insert_with(|| RateLimiterInner::new_with_config(self.default_config));
199 bucket.try_acquire_scaled(multiplier)
200 }
201
202 pub fn acquire(&mut self, tool_name: &str) -> Result<()> {
204 self.try_acquire_for(tool_name)
205 }
206
207 pub fn is_limited(&mut self, tool_name: &str) -> bool {
209 if let Some(bucket) = self.buckets.get_mut(tool_name) {
210 bucket.refill(1.0); bucket.tokens == 0
212 } else {
213 false
214 }
215 }
216
217 pub fn reset_tool(&mut self, tool_name: &str) {
219 if let Some(bucket) = self.buckets.get_mut(tool_name) {
220 bucket.tokens = bucket.config.burst;
221 bucket.last_refill = Instant::now();
222 }
223 }
224}
225
226pub static PER_TOOL_RATE_LIMITER: Lazy<Mutex<PerToolRateLimiter>> =
228 Lazy::new(|| Mutex::new(PerToolRateLimiter::new()));
229
230pub fn try_acquire() -> Result<()> {
234 let mut guard: MutexGuard<'_, RateLimiterInner> = GLOBAL_RATE_LIMITER
235 .lock()
236 .map_err(|e| anyhow!("rate limiter poisoned: {}", e))?;
237 guard.try_acquire()
238}
239
240pub fn try_acquire_for(tool_name: &str) -> Result<()> {
243 let mut guard = PER_TOOL_RATE_LIMITER
244 .lock()
245 .map_err(|e| anyhow!("per-tool rate limiter poisoned: {}", e))?;
246 guard.try_acquire_for(tool_name)
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_global_limiter_allows_burst() {
255 let mut limiter = RateLimiterInner::new();
256 for _ in 0..limiter.config.burst {
258 limiter.try_acquire().unwrap();
259 }
260 assert!(limiter.try_acquire().is_err());
262 }
263
264 #[test]
265 fn test_per_tool_limiter_isolates_tools() {
266 let mut limiter = PerToolRateLimiter::new();
267 for _ in 0..5 {
269 let _ = limiter.try_acquire_for("tool_a");
270 }
271 limiter.try_acquire_for("tool_b").unwrap();
273 }
274
275 #[test]
276 fn test_reset_tool_restores_tokens() {
277 let mut limiter = PerToolRateLimiter::new();
278 let burst = limiter.default_config.burst;
280 for _ in 0..burst {
281 let _ = limiter.try_acquire_for("tool_x");
282 }
283 assert!(limiter.is_limited("tool_x"));
284 limiter.reset_tool("tool_x");
286 assert!(!limiter.is_limited("tool_x"));
287 }
288}