1#[cfg(feature = "shuttle-test")]
6use std::sync::Arc;
7use {
8 cfg_if::cfg_if,
9 dashmap::{DashMap, mapref::entry::Entry},
10 solana_svm_type_overrides::sync::atomic::{AtomicU64, AtomicUsize, Ordering},
11 std::{borrow::Borrow, cmp::Reverse, hash::Hash, time::Instant},
12};
13
14pub struct TokenBucket {
20 new_tokens_per_us: f64,
21 max_tokens: u64,
22 base_time: Instant,
24 tokens: AtomicU64,
25 last_update: AtomicU64,
27 credit_time_us: AtomicU64,
29 #[cfg(feature = "shuttle-test")]
32 pub time_us_override: Arc<AtomicU64>,
33}
34
35impl TokenBucket {
38 pub fn new(initial_tokens: u64, max_tokens: u64, new_tokens_per_second: f64) -> Self {
40 assert!(
41 new_tokens_per_second > 0.0,
42 "Token bucket can not have zero influx rate"
43 );
44 assert!(
45 initial_tokens <= max_tokens,
46 "Can not have more initial tokens than max tokens"
47 );
48 let base_time = Instant::now();
49 TokenBucket {
50 new_tokens_per_us: new_tokens_per_second / 1e6,
52 max_tokens,
53 tokens: AtomicU64::new(initial_tokens),
54 last_update: AtomicU64::new(0),
55 base_time,
56 credit_time_us: AtomicU64::new(0),
57 #[cfg(feature = "shuttle-test")]
58 time_us_override: Arc::new(AtomicU64::new(0)),
59 }
60 }
61
62 #[inline]
66 pub fn current_tokens(&self) -> u64 {
67 let now = self.time_us();
68 self.update_state(now);
69 self.tokens.load(Ordering::Relaxed)
70 }
71
72 #[inline]
77 pub fn consume_tokens(&self, request_size: u64) -> Result<u64, u64> {
78 let now = self.time_us();
79 self.update_state(now);
80 match self.tokens.fetch_update(
81 Ordering::AcqRel, Ordering::Acquire, |tokens| {
84 if tokens >= request_size {
85 Some(tokens.saturating_sub(request_size))
86 } else {
87 None
88 }
89 },
90 ) {
91 Ok(prev) => Ok(prev.saturating_sub(request_size)),
92 Err(prev) => Err(request_size.saturating_sub(prev)),
93 }
94 }
95
96 #[inline]
104 pub fn consume_tokens_saturating(&self, request_size: u64) -> u64 {
105 let now = self.time_us();
106 self.update_state(now);
107 let mut consumed = 0u64;
108 let _ = self.tokens.fetch_update(
109 Ordering::AcqRel, Ordering::Acquire, |tokens| {
112 consumed = tokens.min(request_size);
113 Some(tokens.saturating_sub(consumed))
114 },
115 );
116 consumed
117 }
118
119 #[inline]
121 pub fn add_tokens(&self, new_tokens: u64) {
122 let _ = self.tokens.fetch_update(
123 Ordering::AcqRel, Ordering::Acquire, |tokens| Some(tokens.saturating_add(new_tokens).min(self.max_tokens)),
126 );
127 }
128
129 #[inline]
136 pub fn us_to_have_tokens(&self, num_tokens: u64) -> Option<u64> {
137 if num_tokens > self.max_tokens {
138 return None;
139 }
140
141 match num_tokens.checked_sub(self.current_tokens()) {
142 Some(missing) => Some((missing as f64 / self.new_tokens_per_us) as u64),
143 None => Some(0),
144 }
145 }
146
147 fn time_us(&self) -> u64 {
149 cfg_if! {
150 if #[cfg(feature="shuttle-test")] {
151 self.time_us_override.load(Ordering::Relaxed)
152 } else {
153 let now = Instant::now();
154 let elapsed = now.saturating_duration_since(self.base_time);
155 elapsed.as_micros() as u64
156 }
157 }
158 }
159
160 fn update_state(&self, now: u64) {
163 let last = self.last_update.load(Ordering::SeqCst);
165
166 if now <= last {
168 return;
169 }
170
171 match self.last_update.compare_exchange(
177 last,
178 now,
179 Ordering::AcqRel, Ordering::Acquire, ) {
182 Ok(_) => {
183 let elapsed = now.saturating_sub(last);
185
186 let elapsed =
189 elapsed.saturating_add(self.credit_time_us.swap(0, Ordering::Relaxed));
190
191 let new_tokens_f64 = elapsed as f64 * self.new_tokens_per_us;
192
193 let new_tokens = new_tokens_f64.floor() as u64;
195
196 let time_to_return = if new_tokens >= 1 {
197 self.add_tokens(new_tokens);
199 (new_tokens_f64.fract() / self.new_tokens_per_us) as u64
202 } else {
203 elapsed
205 };
206 self.credit_time_us
208 .fetch_add(time_to_return, Ordering::Relaxed);
209 }
210 Err(_) => {
211 }
213 }
214 }
215}
216
217impl Clone for TokenBucket {
218 fn clone(&self) -> Self {
222 Self {
223 new_tokens_per_us: self.new_tokens_per_us,
224 max_tokens: self.max_tokens,
225 base_time: self.base_time,
226 tokens: AtomicU64::new(self.tokens.load(Ordering::Relaxed)),
227 last_update: AtomicU64::new(self.last_update.load(Ordering::Relaxed)),
228 credit_time_us: AtomicU64::new(self.credit_time_us.load(Ordering::Relaxed)),
229 #[cfg(feature = "shuttle-test")]
231 time_us_override: Arc::clone(&self.time_us_override),
232 }
233 }
234}
235
236pub struct KeyedRateLimiter<K>
245where
246 K: Hash + Eq,
247{
248 data: DashMap<K, TokenBucket>,
249 target_capacity: usize,
250 prototype_bucket: TokenBucket,
251 countdown_to_shrink: AtomicUsize,
252 approx_len: AtomicUsize,
253 shrink_interval: usize,
254}
255
256impl<K> KeyedRateLimiter<K>
257where
258 K: Hash + Eq,
259{
260 #[allow(clippy::arithmetic_side_effects)]
267 pub fn new(target_capacity: usize, prototype_bucket: TokenBucket, shard_amount: usize) -> Self {
268 assert!(
269 shard_amount > 0 && shard_amount.is_power_of_two(),
270 "KeyedRateLimiter shard_amount ({shard_amount}) must be > 0 and a power of two"
271 );
272 assert!(
273 target_capacity >= shard_amount,
274 "KeyedRateLimiter target_capacity ({target_capacity}) must be >= shard_amount \
275 ({shard_amount})"
276 );
277 let shrink_interval = target_capacity / 4;
278 Self {
279 data: DashMap::with_capacity_and_shard_amount(target_capacity * 2, shard_amount),
280 target_capacity,
281 prototype_bucket,
282 countdown_to_shrink: AtomicUsize::new(shrink_interval),
283 approx_len: AtomicUsize::new(0),
284 shrink_interval,
285 }
286 }
287
288 #[inline]
292 pub fn current_tokens(&self, key: impl Borrow<K>) -> Option<u64> {
293 let bucket = self.data.get(key.borrow())?;
294 Some(bucket.current_tokens())
295 }
296
297 pub fn consume_tokens(&self, key: K, request_size: u64) -> Result<u64, u64> {
304 let (entry_added, res) = {
305 let bucket = self.data.entry(key);
306 match bucket {
307 Entry::Occupied(entry) => (false, entry.get().consume_tokens(request_size)),
308 Entry::Vacant(entry) => {
309 let bucket = self.prototype_bucket.clone();
311 let res = bucket.consume_tokens(request_size);
312 entry.insert(bucket);
313 (true, res)
314 }
315 }
316 };
317
318 if entry_added {
319 if let Ok(count) =
320 self.countdown_to_shrink
321 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
322 if v == 0 {
323 None
326 } else {
327 Some(v.saturating_sub(1))
328 }
329 })
330 {
331 if count == 1 {
332 self.maybe_shrink();
334 self.countdown_to_shrink
335 .store(self.shrink_interval, Ordering::Relaxed);
336 }
337 } else {
338 self.approx_len.fetch_add(1, Ordering::Relaxed);
339 }
340 }
341 res
342 }
343
344 #[inline]
347 pub fn len_approx(&self) -> usize {
348 self.approx_len.load(Ordering::Relaxed)
349 }
350
351 #[allow(clippy::arithmetic_side_effects)]
355 fn maybe_shrink(&self) {
356 let mut actual_len = 0;
357 let target_shard_size = self.target_capacity / self.data.shards().len();
358 if target_shard_size == 0 {
359 return;
360 }
361 let mut entries = Vec::with_capacity(target_shard_size * 2);
362 for shardlock in self.data.shards() {
363 let mut shard = shardlock.write();
364
365 if shard.len() <= target_shard_size * 3 / 2 {
366 actual_len += shard.len();
367 continue;
368 }
369 entries.clear();
370 entries.extend(
371 shard.drain().map(|(key, value)| {
372 (key, value.get().last_update.load(Ordering::SeqCst), value)
373 }),
374 );
375
376 entries.select_nth_unstable_by_key(target_shard_size, |(_, last_update, _)| {
377 Reverse(*last_update)
378 });
379
380 shard.extend(
381 entries
382 .drain(..)
383 .take(target_shard_size)
384 .map(|(key, _last_update, value)| (key, value)),
385 );
386 debug_assert!(shard.len() <= target_shard_size);
387 actual_len += shard.len();
388 }
389 self.approx_len.store(actual_len, Ordering::Relaxed);
390 }
391
392 pub fn set_shrink_interval(&mut self, interval: usize) {
397 self.shrink_interval = interval;
398 }
399
400 pub fn shrink_interval(&self) -> usize {
402 self.shrink_interval
403 }
404}
405
406#[cfg(test)]
407pub mod test {
408 use {
409 super::*,
410 solana_svm_type_overrides::thread,
411 std::{
412 net::{IpAddr, Ipv4Addr},
413 time::Duration,
414 },
415 };
416
417 #[test]
418 fn test_token_bucket_basics() {
419 let tb = TokenBucket::new(100, 100, 1000.0);
420 assert_eq!(tb.current_tokens(), 100);
421 tb.consume_tokens(50).expect("Bucket is initially full");
422 tb.consume_tokens(50)
423 .expect("We should still have >50 tokens left");
424 tb.consume_tokens(50)
425 .expect_err("There should not be enough tokens now");
426 thread::sleep(Duration::from_millis(50));
427 assert!(
428 tb.current_tokens() > 40,
429 "We should be refilling at ~1 token per millisecond"
430 );
431 assert!(
432 tb.current_tokens() < 70,
433 "We should be refilling at ~1 token per millisecond"
434 );
435 tb.consume_tokens(40)
436 .expect("Bucket should have enough for another request now");
437 thread::sleep(Duration::from_millis(120));
438 assert_eq!(tb.current_tokens(), 100, "Bucket should not overfill");
439 }
440
441 #[test]
442 fn test_consume_tokens_saturating_consume() {
443 let tb = TokenBucket::new(100, 100, 0.00001);
445
446 let consumed = tb.consume_tokens_saturating(42);
447 assert_eq!(consumed, 42, "Should have consumed exactly 42 tokens");
448 assert_eq!(
449 tb.current_tokens(),
450 58,
451 "Bucket should have 58 tokens after consuming 42"
452 );
453
454 let consumed = tb.consume_tokens_saturating(100);
455 assert_eq!(consumed, 58, "Should have consumed all available tokens");
456 assert_eq!(
457 tb.current_tokens(),
458 0,
459 "Bucket should be empty after full consume"
460 );
461
462 let consumed = tb.consume_tokens_saturating(10);
463 assert_eq!(
464 consumed, 0,
465 "Should have consumed 0 tokens as bucket is empty"
466 );
467 let consumed = tb.consume_tokens_saturating(0);
468 assert_eq!(consumed, 0);
469 assert_eq!(
470 tb.current_tokens(),
471 0,
472 "Bucket should be empty after full consume"
473 );
474 }
475
476 #[test]
477 fn test_token_bucket_us_to_have_tokens() {
478 let tb = TokenBucket::new(1000, 1000, 1000.0);
479 assert_eq!(tb.current_tokens(), 1000);
480 tb.consume_tokens(1000).expect("Bucket is initially full");
481 assert!(
482 tb.current_tokens() < 100,
483 "Shoult not have many tokens left in bucket"
484 );
485
486 let t = tb
487 .us_to_have_tokens(500)
488 .expect("500 < bucket capacity (1000)")
489 / 1000; assert!(t > 100, "time to fill should be ~ 500ms (got {t})");
491 assert!(t <= 500, "time to fill should be less than 500ms (got {t})");
492 }
493
494 #[test]
495 fn test_keyed_rate_limiter() {
496 let prototype_bucket = TokenBucket::new(100, 100, 1000.0);
497 let rl = KeyedRateLimiter::new(8, prototype_bucket, 2);
498 let ip1 = IpAddr::V4(Ipv4Addr::from_bits(1234));
499 let ip2 = IpAddr::V4(Ipv4Addr::from_bits(4321));
500 assert_eq!(rl.current_tokens(ip1), None, "Initially no buckets exist");
501 rl.consume_tokens(ip1, 50)
502 .expect("Bucket is initially full");
503 rl.consume_tokens(ip1, 50)
504 .expect("We should still have >50 tokens left");
505 rl.consume_tokens(ip1, 50)
506 .expect_err("There should not be enough tokens now");
507 rl.consume_tokens(ip2, 50)
508 .expect("Bucket is initially full");
509 rl.consume_tokens(ip2, 50)
510 .expect("We should still have >50 tokens left");
511 rl.consume_tokens(ip2, 50)
512 .expect_err("There should not be enough tokens now");
513 std::thread::sleep(Duration::from_millis(50));
514 assert!(
515 rl.current_tokens(ip1).unwrap() > 40,
516 "We should be refilling at ~1 token per millisecond"
517 );
518 assert!(
519 rl.current_tokens(ip1).unwrap() < 70,
520 "We should be refilling at ~1 token per millisecond"
521 );
522 rl.consume_tokens(ip1, 40)
523 .expect("Bucket should have enough for another request now");
524 thread::sleep(Duration::from_millis(120));
525 assert_eq!(
526 rl.current_tokens(ip1),
527 Some(100),
528 "Bucket should not overfill"
529 );
530 assert_eq!(
531 rl.current_tokens(ip2),
532 Some(100),
533 "Bucket should not overfill"
534 );
535
536 rl.consume_tokens(ip2, 100).expect("Bucket should be full");
537 for ip in 0..64 {
540 let ip = IpAddr::V4(Ipv4Addr::from_bits(ip));
541 rl.consume_tokens(ip, 50).unwrap();
542 }
543 assert_eq!(
544 rl.current_tokens(ip1),
545 None,
546 "Very old record should have been erased"
547 );
548 rl.consume_tokens(ip2, 100)
549 .expect("New bucket should have been made for ip2");
550 }
551
552 #[test]
553 #[should_panic(expected = "must be >= shard_amount")]
554 fn test_keyed_rate_limiter_capacity_less_than_shards_panics() {
555 let tb = TokenBucket::new(1, 1, 1.0);
556 let _ = KeyedRateLimiter::<u64>::new(1, tb, 2);
558 }
559
560 #[cfg(feature = "shuttle-test")]
561 #[test]
562 fn shuttle_test_consume_tokens_saturating_race() {
563 use {shuttle::sync::atomic::AtomicBool, std::sync::Arc};
564 shuttle::check_random(
565 || {
566 let test_duration_us = 2500;
567 let run = Arc::new(AtomicBool::new(true));
568 let tb = Arc::new(TokenBucket::new(10, 20, 5000.0));
569 let time = Arc::clone(&tb.time_us_override);
570
571 let time_advancer = {
573 let run = Arc::clone(&run);
574 thread::spawn(move || {
575 let mut current_time = 0;
576 while current_time < test_duration_us && run.load(Ordering::SeqCst) {
577 let increment = 100; current_time += increment;
579 time.store(current_time, Ordering::SeqCst);
580 shuttle::thread::yield_now();
581 }
582 run.store(false, Ordering::SeqCst);
583 })
584 };
585
586 let threads: Vec<_> = (0..2)
587 .map(|_| {
588 let run = Arc::clone(&run);
589 let tb = Arc::clone(&tb);
590 thread::spawn(move || {
591 let mut total = 0u64;
592 while run.load(Ordering::SeqCst) {
593 total += tb.consume_tokens_saturating(5);
594 shuttle::thread::yield_now();
595 }
596 total
597 })
598 })
599 .collect();
600
601 time_advancer.join().unwrap();
602 let received: u64 = threads.into_iter().map(|t| t.join().unwrap()).sum();
603
604 assert!(
609 received <= 23,
610 "Should not consume more tokens than were minted: {received}"
611 );
612 assert!(
613 received >= 10,
614 "Should consume at least the initial tokens: {received}"
615 );
616 },
617 100,
618 );
619 }
620
621 #[cfg(feature = "shuttle-test")]
622 #[test]
623 fn shuttle_test_token_bucket_race() {
624 use {shuttle::sync::atomic::AtomicBool, std::sync::Arc};
625 shuttle::check_random(
626 || {
627 let test_duration_us = 2500;
628 let run = Arc::new(AtomicBool::new(true));
629 let tb = Arc::new(TokenBucket::new(10, 20, 5000.0));
630 let time = Arc::clone(&tb.time_us_override);
631
632 let time_advancer = {
634 let run = Arc::clone(&run);
635 thread::spawn(move || {
636 let mut current_time = 0;
637 while current_time < test_duration_us && run.load(Ordering::SeqCst) {
638 let increment = 100; current_time += increment;
640 time.store(current_time, Ordering::SeqCst);
641 shuttle::thread::yield_now();
642 }
643 run.store(false, Ordering::SeqCst);
644 })
645 };
646
647 let threads: Vec<_> = (0..2)
648 .map(|_| {
649 let run = Arc::clone(&run);
650 let tb = Arc::clone(&tb);
651 thread::spawn(move || {
652 let mut total = 0;
653 while run.load(Ordering::SeqCst) {
654 if tb.consume_tokens(5).is_ok() {
655 total += 1;
656 }
657 shuttle::thread::yield_now();
658 }
659 total
660 })
661 })
662 .collect();
663
664 time_advancer.join().unwrap();
665 let received = threads.into_iter().map(|t| t.join().unwrap()).sum();
666
667 assert_eq!(4, received);
671 },
672 100,
673 );
674 }
675}