1use aws_smithy_async::time::TimeSource;
7use aws_smithy_types::config_bag::{Storable, StoreReplace};
8use aws_smithy_types::retry::ErrorKind;
9use std::fmt;
10use std::sync::atomic::AtomicU32;
11use std::sync::atomic::Ordering;
12use std::sync::Arc;
13use std::time::{Duration, SystemTime};
14use tokio::sync::{OwnedSemaphorePermit, Semaphore};
15
16pub(crate) const DEFAULT_CAPACITY: usize = 500;
17pub const MAXIMUM_CAPACITY: usize = 500_000_000;
24#[allow(dead_code)]
25pub(crate) const DEFAULT_RETRY_COST: u32 = 14;
26#[allow(dead_code)]
27pub(crate) const DEFAULT_RETRY_TIMEOUT_COST: u32 = 14;
28#[allow(dead_code)]
29pub(crate) const THROTTLING_RETRY_COST: u32 = 5;
30
31const LEGACY_RETRY_COST: u32 = 5;
33const LEGACY_RETRY_TIMEOUT_COST: u32 = LEGACY_RETRY_COST * 2;
34const PERMIT_REGENERATION_AMOUNT: usize = 1;
35const DEFAULT_SUCCESS_REWARD: f32 = 0.0;
36
37#[derive(Clone, Debug)]
62pub struct TokenBucket {
63 semaphore: Arc<Semaphore>,
64 max_permits: usize,
65 timeout_retry_cost: u32,
66 retry_cost: u32,
67 throttling_retry_cost: u32,
68 success_reward: f32,
69 fractional_tokens: Arc<AtomicF32>,
70 refill_rate: f32,
71 last_refill_time_secs: Arc<AtomicU32>,
74}
75
76impl std::panic::UnwindSafe for AtomicF32 {}
77impl std::panic::RefUnwindSafe for AtomicF32 {}
78struct AtomicF32 {
79 storage: AtomicU32,
80}
81impl AtomicF32 {
82 fn new(value: f32) -> Self {
83 let as_u32 = value.to_bits();
84 Self {
85 storage: AtomicU32::new(as_u32),
86 }
87 }
88 fn store(&self, value: f32) {
89 let as_u32 = value.to_bits();
90 self.storage.store(as_u32, Ordering::Relaxed)
91 }
92 fn load(&self) -> f32 {
93 let as_u32 = self.storage.load(Ordering::Relaxed);
94 f32::from_bits(as_u32)
95 }
96}
97
98impl fmt::Debug for AtomicF32 {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 f.debug_struct("AtomicF32")
102 .field("value", &self.load())
103 .finish()
104 }
105}
106
107impl Clone for AtomicF32 {
108 fn clone(&self) -> Self {
109 AtomicF32 {
111 storage: AtomicU32::new(self.storage.load(Ordering::Relaxed)),
112 }
113 }
114}
115
116impl Storable for TokenBucket {
117 type Storer = StoreReplace<Self>;
118}
119
120impl Default for TokenBucket {
121 fn default() -> Self {
122 Self {
123 semaphore: Arc::new(Semaphore::new(DEFAULT_CAPACITY)),
124 max_permits: DEFAULT_CAPACITY,
125 timeout_retry_cost: LEGACY_RETRY_TIMEOUT_COST,
126 retry_cost: LEGACY_RETRY_COST,
127 throttling_retry_cost: LEGACY_RETRY_COST,
128 success_reward: DEFAULT_SUCCESS_REWARD,
129 fractional_tokens: Arc::new(AtomicF32::new(0.0)),
130 refill_rate: 0.0,
131 last_refill_time_secs: Arc::new(AtomicU32::new(0)),
132 }
133 }
134}
135
136impl TokenBucket {
137 pub fn new(initial_quota: usize) -> Self {
139 Self {
140 semaphore: Arc::new(Semaphore::new(initial_quota)),
141 max_permits: initial_quota,
142 ..Default::default()
143 }
144 }
145
146 pub fn unlimited() -> Self {
148 Self {
149 semaphore: Arc::new(Semaphore::new(MAXIMUM_CAPACITY)),
150 max_permits: MAXIMUM_CAPACITY,
151 timeout_retry_cost: 0,
152 retry_cost: 0,
153 throttling_retry_cost: 0,
154 success_reward: 0.0,
155 fractional_tokens: Arc::new(AtomicF32::new(0.0)),
156 refill_rate: 0.0,
157 last_refill_time_secs: Arc::new(AtomicU32::new(0)),
158 }
159 }
160
161 pub fn builder() -> TokenBucketBuilder {
163 TokenBucketBuilder::default()
164 }
165
166 pub(crate) fn acquire(
167 &self,
168 err: &ErrorKind,
169 time_source: &impl TimeSource,
170 ) -> Option<OwnedSemaphorePermit> {
171 self.refill_tokens_based_on_time(time_source);
173 self.convert_fractional_tokens();
175
176 let retry_cost = match err {
177 ErrorKind::TransientError => self.timeout_retry_cost,
178 ErrorKind::ThrottlingError => self.throttling_retry_cost,
179 _ => self.retry_cost,
180 };
181
182 self.semaphore
183 .clone()
184 .try_acquire_many_owned(retry_cost)
185 .ok()
186 }
187
188 pub(crate) fn success_reward(&self) -> f32 {
189 self.success_reward
190 }
191
192 pub(crate) fn regenerate_a_token(&self) {
193 self.add_permits(PERMIT_REGENERATION_AMOUNT);
194 }
195
196 #[inline]
200 fn convert_fractional_tokens(&self) {
201 let mut calc_fractional_tokens = self.fractional_tokens.load();
202 if !calc_fractional_tokens.is_finite() {
204 tracing::error!(
205 "Fractional tokens corrupted to: {}, resetting to 0.0",
206 calc_fractional_tokens
207 );
208 self.fractional_tokens.store(0.0);
209 return;
210 }
211
212 let full_tokens_accumulated = calc_fractional_tokens.floor();
213 if full_tokens_accumulated >= 1.0 {
214 self.add_permits(full_tokens_accumulated as usize);
215 calc_fractional_tokens -= full_tokens_accumulated;
216 }
217 self.fractional_tokens.store(calc_fractional_tokens);
219 }
220
221 #[inline]
225 fn refill_tokens_based_on_time(&self, time_source: &impl TimeSource) {
226 if self.refill_rate > 0.0 {
227 let current_time_secs = time_source
229 .now()
230 .duration_since(SystemTime::UNIX_EPOCH)
231 .unwrap_or(Duration::ZERO)
232 .as_secs() as u32;
233
234 let last_refill_secs = self.last_refill_time_secs.load(Ordering::Relaxed);
235
236 if current_time_secs == last_refill_secs {
238 return;
239 }
240
241 if self
244 .last_refill_time_secs
245 .compare_exchange(
246 last_refill_secs,
247 current_time_secs,
248 Ordering::Relaxed,
249 Ordering::Relaxed,
250 )
251 .is_err()
252 {
253 return;
255 }
256
257 let current_fractional = self.fractional_tokens.load();
259 let max_fractional = self.max_permits as f32;
260
261 if current_fractional >= max_fractional {
263 return;
264 }
265
266 let elapsed_secs = current_time_secs.saturating_sub(last_refill_secs);
267 let tokens_to_add = elapsed_secs as f32 * self.refill_rate;
268
269 let new_fractional = (current_fractional + tokens_to_add).min(max_fractional);
271 self.fractional_tokens.store(new_fractional);
272 }
273 }
274
275 #[inline]
276 pub(crate) fn reward_success(&self) {
277 if self.success_reward > 0.0 {
278 let current = self.fractional_tokens.load();
279 let max_fractional = self.max_permits as f32;
280 if current >= max_fractional {
282 return;
283 }
284 let new_fractional = (current + self.success_reward).min(max_fractional);
286 self.fractional_tokens.store(new_fractional);
287 }
288 }
289
290 pub(crate) fn add_permits(&self, amount: usize) {
291 let available = self.semaphore.available_permits();
292 if available >= self.max_permits {
293 return;
294 }
295 self.semaphore
296 .add_permits(amount.min(self.max_permits - available));
297 }
298
299 pub fn is_full(&self) -> bool {
301 self.convert_fractional_tokens();
302 self.semaphore.available_permits() >= self.max_permits
303 }
304
305 pub fn is_empty(&self) -> bool {
307 self.convert_fractional_tokens();
308 self.semaphore.available_permits() == 0
309 }
310
311 #[allow(dead_code)] #[cfg(any(test, feature = "test-util", feature = "legacy-test-util"))]
313 pub(crate) fn available_permits(&self) -> usize {
314 self.semaphore.available_permits()
315 }
316
317 #[allow(dead_code)]
319 #[doc(hidden)]
320 #[cfg(any(test, feature = "test-util", feature = "legacy-test-util"))]
321 pub fn last_refill_time_secs(&self) -> Arc<AtomicU32> {
322 self.last_refill_time_secs.clone()
323 }
324}
325
326#[derive(Clone, Debug, Default)]
328pub struct TokenBucketBuilder {
329 capacity: Option<usize>,
330 retry_cost: Option<u32>,
331 throttling_retry_cost: Option<u32>,
332 timeout_retry_cost: Option<u32>,
333 success_reward: Option<f32>,
334 refill_rate: Option<f32>,
335}
336
337impl TokenBucketBuilder {
338 pub fn new() -> Self {
340 Self::default()
341 }
342
343 pub fn capacity(mut self, mut capacity: usize) -> Self {
345 if capacity > MAXIMUM_CAPACITY {
346 capacity = MAXIMUM_CAPACITY;
347 }
348 self.capacity = Some(capacity);
349 self
350 }
351
352 pub fn retry_cost(mut self, retry_cost: u32) -> Self {
354 self.retry_cost = Some(retry_cost);
355 self
356 }
357
358 pub fn throttling_retry_cost(mut self, throttling_retry_cost: u32) -> Self {
360 self.throttling_retry_cost = Some(throttling_retry_cost);
361 self
362 }
363
364 pub fn timeout_retry_cost(mut self, timeout_retry_cost: u32) -> Self {
366 self.timeout_retry_cost = Some(timeout_retry_cost);
367 self
368 }
369
370 pub fn success_reward(mut self, reward: f32) -> Self {
372 self.success_reward = Some(reward);
373 self
374 }
375
376 pub fn refill_rate(mut self, rate: f32) -> Self {
381 let validated_rate = if rate.is_finite() { rate.max(0.0) } else { 0.0 };
382 self.refill_rate = Some(validated_rate);
383 self
384 }
385
386 pub fn build(self) -> TokenBucket {
388 TokenBucket {
389 semaphore: Arc::new(Semaphore::new(self.capacity.unwrap_or(DEFAULT_CAPACITY))),
390 max_permits: self.capacity.unwrap_or(DEFAULT_CAPACITY),
391 retry_cost: self.retry_cost.unwrap_or(LEGACY_RETRY_COST),
392 throttling_retry_cost: self.throttling_retry_cost.unwrap_or(LEGACY_RETRY_COST),
393 timeout_retry_cost: self.timeout_retry_cost.unwrap_or(LEGACY_RETRY_TIMEOUT_COST),
394 success_reward: self.success_reward.unwrap_or(DEFAULT_SUCCESS_REWARD),
395 fractional_tokens: Arc::new(AtomicF32::new(0.0)),
396 refill_rate: self.refill_rate.unwrap_or(0.0),
397 last_refill_time_secs: Arc::new(AtomicU32::new(0)),
398 }
399 }
400}
401
402#[cfg(test)]
403mod tests {
404
405 use super::*;
406 use aws_smithy_async::test_util::ManualTimeSource;
407 use std::{sync::LazyLock, time::UNIX_EPOCH};
408
409 static TIME_SOURCE: LazyLock<ManualTimeSource> =
410 LazyLock::new(|| ManualTimeSource::new(UNIX_EPOCH + Duration::from_secs(12344321)));
411
412 #[test]
413 fn test_unlimited_token_bucket() {
414 let bucket = TokenBucket::unlimited();
415
416 assert!(bucket
418 .acquire(&ErrorKind::ThrottlingError, &*TIME_SOURCE)
419 .is_some());
420 assert!(bucket
421 .acquire(&ErrorKind::TransientError, &*TIME_SOURCE)
422 .is_some());
423
424 assert_eq!(bucket.max_permits, MAXIMUM_CAPACITY);
426
427 assert_eq!(bucket.retry_cost, 0);
429 assert_eq!(bucket.timeout_retry_cost, 0);
430
431 let mut permits = Vec::new();
433 for _ in 0..100 {
434 let permit = bucket.acquire(&ErrorKind::ThrottlingError, &*TIME_SOURCE);
435 assert!(permit.is_some());
436 permits.push(permit);
437 assert_eq!(MAXIMUM_CAPACITY, bucket.semaphore.available_permits());
439 }
440 }
441
442 #[test]
443 fn test_bounded_permits_exhaustion() {
444 let bucket = TokenBucket::new(10);
445 let mut permits = Vec::new();
446
447 for _ in 0..100 {
448 let permit = bucket.acquire(&ErrorKind::ThrottlingError, &*TIME_SOURCE);
449 if let Some(p) = permit {
450 permits.push(p);
451 } else {
452 break;
453 }
454 }
455
456 assert_eq!(permits.len(), 2); assert!(bucket
460 .acquire(&ErrorKind::ThrottlingError, &*TIME_SOURCE)
461 .is_none());
462 }
463
464 #[test]
465 fn test_fractional_tokens_accumulate_and_convert() {
466 let bucket = TokenBucket::builder()
467 .capacity(10)
468 .success_reward(0.4)
469 .build();
470
471 let _hold_permit = bucket.acquire(&ErrorKind::TransientError, &*TIME_SOURCE);
473 assert_eq!(bucket.semaphore.available_permits(), 0);
474
475 bucket.reward_success();
477 bucket.convert_fractional_tokens();
478 assert_eq!(bucket.semaphore.available_permits(), 0);
479
480 bucket.reward_success();
482 bucket.convert_fractional_tokens();
483 assert_eq!(bucket.semaphore.available_permits(), 0);
484
485 bucket.reward_success();
487 bucket.convert_fractional_tokens();
488 assert_eq!(bucket.semaphore.available_permits(), 1);
489 }
490
491 #[test]
492 fn test_fractional_tokens_respect_max_capacity() {
493 let bucket = TokenBucket::builder()
494 .capacity(10)
495 .success_reward(2.0)
496 .build();
497
498 for _ in 0..20 {
499 bucket.reward_success();
500 }
501
502 assert!(bucket.semaphore.available_permits() == 10);
503 }
504
505 #[test]
506 fn test_convert_fractional_tokens() {
507 let test_cases = [
509 (0.7, 0, 0.7),
510 (1.0, 1, 0.0),
511 (2.3, 2, 0.3),
512 (5.8, 5, 0.8),
513 (10.0, 10, 0.0),
514 (f32::NAN, 0, 0.0),
516 (f32::INFINITY, 0, 0.0),
517 ];
518
519 for (input, expected_permits, expected_remaining) in test_cases {
520 let bucket = TokenBucket::builder().capacity(10).build();
521 let _hold_permit = bucket.acquire(&ErrorKind::TransientError, &*TIME_SOURCE);
522 let initial = bucket.semaphore.available_permits();
523
524 bucket.fractional_tokens.store(input);
525 bucket.convert_fractional_tokens();
526
527 assert_eq!(
528 bucket.semaphore.available_permits() - initial,
529 expected_permits
530 );
531 assert!((bucket.fractional_tokens.load() - expected_remaining).abs() < 0.0001);
532 }
533 }
534
535 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
536 #[test]
537 fn test_builder_with_custom_values() {
538 let bucket = TokenBucket::builder()
539 .capacity(100)
540 .retry_cost(10)
541 .timeout_retry_cost(20)
542 .success_reward(0.5)
543 .refill_rate(2.5)
544 .build();
545
546 assert_eq!(bucket.max_permits, 100);
547 assert_eq!(bucket.retry_cost, 10);
548 assert_eq!(bucket.timeout_retry_cost, 20);
549 assert_eq!(bucket.success_reward, 0.5);
550 assert_eq!(bucket.refill_rate, 2.5);
551 }
552
553 #[test]
554 fn test_builder_refill_rate_validation() {
555 let bucket = TokenBucket::builder().refill_rate(-5.0).build();
557 assert_eq!(bucket.refill_rate, 0.0);
558
559 let bucket = TokenBucket::builder().refill_rate(1.5).build();
561 assert_eq!(bucket.refill_rate, 1.5);
562
563 let bucket = TokenBucket::builder().refill_rate(0.0).build();
565 assert_eq!(bucket.refill_rate, 0.0);
566 }
567
568 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
569 #[test]
570 fn test_builder_custom_time_source() {
571 use aws_smithy_async::test_util::ManualTimeSource;
572 use std::time::UNIX_EPOCH;
573
574 let manual_time = ManualTimeSource::new(UNIX_EPOCH);
576 let bucket = TokenBucket::builder()
577 .capacity(100)
578 .refill_rate(1.0)
579 .build();
580
581 let _permits = bucket.semaphore.try_acquire_many(100).unwrap();
583 assert_eq!(bucket.available_permits(), 0);
584
585 manual_time.advance(Duration::from_secs(5));
587
588 bucket.refill_tokens_based_on_time(&manual_time);
589 bucket.convert_fractional_tokens();
590
591 assert_eq!(bucket.available_permits(), 5);
593 }
594
595 #[test]
596 fn test_atomicf32_f32_to_bits_conversion_correctness() {
597 let test_values = vec![
599 0.0,
600 -0.0,
601 1.0,
602 -1.0,
603 f32::INFINITY,
604 f32::NEG_INFINITY,
605 f32::NAN,
606 f32::MIN,
607 f32::MAX,
608 f32::MIN_POSITIVE,
609 f32::EPSILON,
610 std::f32::consts::PI,
611 std::f32::consts::E,
612 1.23456789e-38, 1.23456789e38, 1.1754944e-38, ];
617
618 for &expected in &test_values {
619 let atomic = AtomicF32::new(expected);
620 let actual = atomic.load();
621
622 if expected.is_nan() {
624 assert!(actual.is_nan(), "Expected NaN, got {}", actual);
625 assert_eq!(expected.to_bits(), actual.to_bits());
627 } else {
628 assert_eq!(expected.to_bits(), actual.to_bits());
629 }
630 }
631 }
632
633 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
634 #[test]
635 fn test_atomicf32_store_load_preserves_exact_bits() {
636 let atomic = AtomicF32::new(0.0);
637
638 let critical_bit_patterns = vec![
641 0x00000000u32, 0x80000000u32, 0x7F800000u32, 0xFF800000u32, 0x7FC00000u32, 0x7FA00000u32, 0x00000001u32, 0x007FFFFFu32, 0x00800000u32, ];
651
652 for &expected_bits in &critical_bit_patterns {
653 let expected_f32 = f32::from_bits(expected_bits);
654 atomic.store(expected_f32);
655 let loaded_f32 = atomic.load();
656 let actual_bits = loaded_f32.to_bits();
657
658 assert_eq!(expected_bits, actual_bits);
659 }
660 }
661
662 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
663 #[test]
664 fn test_atomicf32_concurrent_store_load_safety() {
665 use std::sync::Arc;
666 use std::thread;
667
668 let atomic = Arc::new(AtomicF32::new(0.0));
669 let test_values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
670 let mut handles = Vec::new();
671
672 for &value in &test_values {
674 let atomic_clone = Arc::clone(&atomic);
675 let handle = thread::spawn(move || {
676 for _ in 0..1000 {
677 atomic_clone.store(value);
678 }
679 });
680 handles.push(handle);
681 }
682
683 let atomic_reader = Arc::clone(&atomic);
685 let reader_handle = thread::spawn(move || {
686 let mut readings = Vec::new();
687 for _ in 0..5000 {
688 let value = atomic_reader.load();
689 readings.push(value);
690 }
691 readings
692 });
693
694 for handle in handles {
696 handle.join().expect("Writer thread panicked");
697 }
698
699 let readings = reader_handle.join().expect("Reader thread panicked");
700
701 for &reading in &readings {
704 assert!(test_values.contains(&reading) || reading == 0.0);
705
706 assert!(
709 reading.is_finite() || reading == 0.0,
710 "Corrupted reading detected"
711 );
712 }
713 }
714
715 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
716 #[test]
717 fn test_atomicf32_stress_concurrent_access() {
718 use std::sync::{Arc, Barrier};
719 use std::thread;
720
721 let expected_values = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
722 let atomic = Arc::new(AtomicF32::new(0.0));
723 let barrier = Arc::new(Barrier::new(10)); let mut handles = Vec::new();
725
726 for i in 0..10 {
728 let atomic_clone = Arc::clone(&atomic);
729 let barrier_clone = Arc::clone(&barrier);
730 let handle = thread::spawn(move || {
731 barrier_clone.wait(); for _ in 0..10000 {
735 let value = i as f32;
736 atomic_clone.store(value);
737 let loaded = atomic_clone.load();
738 assert!(loaded >= 0.0 && loaded <= 9.0);
740 assert!(
741 expected_values.contains(&loaded),
742 "Got unexpected value: {}, expected one of {:?}",
743 loaded,
744 expected_values
745 );
746 }
747 });
748 handles.push(handle);
749 }
750
751 for handle in handles {
752 handle.join().unwrap();
753 }
754 }
755
756 #[test]
757 fn test_atomicf32_integration_with_token_bucket_usage() {
758 let atomic = AtomicF32::new(0.0);
759 let success_reward = 0.3;
760 let iterations = 5;
761
762 for _ in 1..=iterations {
764 let current = atomic.load();
765 atomic.store(current + success_reward);
766 }
767
768 let accumulated = atomic.load();
769 let expected_total = iterations as f32 * success_reward; let full_tokens = accumulated.floor();
773 atomic.store(accumulated - full_tokens);
774 let remaining = atomic.load();
775
776 assert_eq!(full_tokens, expected_total.floor()); assert!(remaining >= 0.0 && remaining < 1.0);
779 assert_eq!(remaining, expected_total - expected_total.floor());
780 }
781
782 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
783 #[test]
784 fn test_atomicf32_clone_creates_independent_copy() {
785 let original = AtomicF32::new(123.456);
786 let cloned = original.clone();
787
788 assert_eq!(original.load(), cloned.load());
790
791 original.store(999.0);
793 assert_eq!(
794 cloned.load(),
795 123.456,
796 "Clone should be unaffected by original changes"
797 );
798 assert_eq!(original.load(), 999.0, "Original should have new value");
799 }
800
801 #[test]
802 fn test_combined_time_and_success_rewards() {
803 use aws_smithy_async::test_util::ManualTimeSource;
804 use std::time::UNIX_EPOCH;
805
806 let time_source = ManualTimeSource::new(UNIX_EPOCH);
807 let current_time_secs = UNIX_EPOCH
808 .duration_since(SystemTime::UNIX_EPOCH)
809 .unwrap()
810 .as_secs() as u32;
811
812 let bucket = TokenBucket {
813 refill_rate: 1.0,
814 success_reward: 0.5,
815 last_refill_time_secs: Arc::new(AtomicU32::new(current_time_secs)),
816 semaphore: Arc::new(Semaphore::new(0)),
817 max_permits: 100,
818 ..Default::default()
819 };
820
821 bucket.reward_success();
823 bucket.reward_success();
824
825 time_source.advance(Duration::from_secs(2));
827
828 bucket.refill_tokens_based_on_time(&time_source);
831 bucket.convert_fractional_tokens();
832
833 assert_eq!(bucket.available_permits(), 3);
834 assert!(bucket.fractional_tokens.load().abs() < 0.0001);
835 }
836
837 #[test]
838 fn test_refill_rates() {
839 use aws_smithy_async::test_util::ManualTimeSource;
840 use std::time::UNIX_EPOCH;
841 let test_cases = [
843 (10.0, 2, 20, 0.0), (0.001, 1100, 1, 0.1), (0.0001, 11000, 1, 0.1), (0.001, 1200, 1, 0.2), (0.0001, 10000, 1, 0.0), (0.001, 500, 0, 0.5), ];
850
851 for (refill_rate, elapsed_secs, expected_permits, expected_fractional) in test_cases {
852 let time_source = ManualTimeSource::new(UNIX_EPOCH);
853 let current_time_secs = UNIX_EPOCH
854 .duration_since(SystemTime::UNIX_EPOCH)
855 .unwrap()
856 .as_secs() as u32;
857
858 let bucket = TokenBucket {
859 refill_rate,
860 last_refill_time_secs: Arc::new(AtomicU32::new(current_time_secs)),
861 semaphore: Arc::new(Semaphore::new(0)),
862 max_permits: 100,
863 ..Default::default()
864 };
865
866 time_source.advance(Duration::from_secs(elapsed_secs));
868
869 bucket.refill_tokens_based_on_time(&time_source);
870 bucket.convert_fractional_tokens();
871
872 assert_eq!(
873 bucket.available_permits(),
874 expected_permits,
875 "Rate {}: After {}s expected {} permits",
876 refill_rate,
877 elapsed_secs,
878 expected_permits
879 );
880 assert!(
881 (bucket.fractional_tokens.load() - expected_fractional).abs() < 0.0001,
882 "Rate {}: After {}s expected {} fractional, got {}",
883 refill_rate,
884 elapsed_secs,
885 expected_fractional,
886 bucket.fractional_tokens.load()
887 );
888 }
889 }
890
891 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
892 #[test]
893 fn test_rewards_capped_at_max_capacity() {
894 use aws_smithy_async::test_util::ManualTimeSource;
895 use std::time::UNIX_EPOCH;
896
897 let time_source = ManualTimeSource::new(UNIX_EPOCH);
898 let current_time_secs = UNIX_EPOCH
899 .duration_since(SystemTime::UNIX_EPOCH)
900 .unwrap()
901 .as_secs() as u32;
902
903 let bucket = TokenBucket {
904 refill_rate: 50.0,
905 success_reward: 2.0,
906 last_refill_time_secs: Arc::new(AtomicU32::new(current_time_secs)),
907 semaphore: Arc::new(Semaphore::new(5)),
908 max_permits: 10,
909 ..Default::default()
910 };
911
912 for _ in 0..50 {
914 bucket.reward_success();
915 }
916
917 assert_eq!(bucket.fractional_tokens.load(), 10.0);
919
920 time_source.advance(Duration::from_secs(100));
922
923 bucket.refill_tokens_based_on_time(&time_source);
926
927 assert_eq!(
929 bucket.fractional_tokens.load(),
930 10.0,
931 "Fractional tokens should be capped at max_permits"
932 );
933 bucket.convert_fractional_tokens();
935 assert_eq!(bucket.available_permits(), 10);
936 }
937
938 #[cfg(any(feature = "test-util", feature = "legacy-test-util"))]
939 #[test]
940 fn test_concurrent_time_based_refill_no_over_generation() {
941 use aws_smithy_async::test_util::ManualTimeSource;
942 use std::sync::{Arc, Barrier};
943 use std::thread;
944 use std::time::UNIX_EPOCH;
945
946 let time_source = ManualTimeSource::new(UNIX_EPOCH);
947 let current_time_secs = UNIX_EPOCH
948 .duration_since(SystemTime::UNIX_EPOCH)
949 .unwrap()
950 .as_secs() as u32;
951
952 let bucket = Arc::new(TokenBucket {
954 refill_rate: 1.0,
955 last_refill_time_secs: Arc::new(AtomicU32::new(current_time_secs)),
956 semaphore: Arc::new(Semaphore::new(0)),
957 max_permits: 100,
958 ..Default::default()
959 });
960
961 time_source.advance(Duration::from_secs(10));
963 let shared_time_source = aws_smithy_async::time::SharedTimeSource::new(time_source);
964
965 let barrier = Arc::new(Barrier::new(100));
967 let mut handles = Vec::new();
968
969 for _ in 0..100 {
970 let bucket_clone1 = Arc::clone(&bucket);
971 let barrier_clone1 = Arc::clone(&barrier);
972 let time_source_clone1 = shared_time_source.clone();
973 let bucket_clone2 = Arc::clone(&bucket);
974 let barrier_clone2 = Arc::clone(&barrier);
975 let time_source_clone2 = shared_time_source.clone();
976
977 let handle1 = thread::spawn(move || {
978 barrier_clone1.wait();
980
981 bucket_clone1.refill_tokens_based_on_time(&time_source_clone1);
983 });
984
985 let handle2 = thread::spawn(move || {
986 barrier_clone2.wait();
988
989 bucket_clone2.refill_tokens_based_on_time(&time_source_clone2);
991 });
992 handles.push(handle1);
993 handles.push(handle2);
994 }
995
996 for handle in handles {
998 handle.join().unwrap();
999 }
1000
1001 bucket.convert_fractional_tokens();
1003
1004 assert_eq!(
1007 bucket.available_permits(),
1008 10,
1009 "Only one thread should have added tokens, not all 100"
1010 );
1011
1012 assert!(bucket.fractional_tokens.load().abs() < 0.0001);
1014 }
1015
1016 #[test]
1018 fn test_is_full_accounts_for_fractional_tokens() {
1019 let bucket = TokenBucket::builder()
1020 .capacity(2)
1021 .retry_cost(1)
1022 .success_reward(0.9)
1023 .build();
1024
1025 assert!(bucket.is_full());
1026
1027 let _p1 = bucket
1028 .acquire(&ErrorKind::ServerError, &*TIME_SOURCE)
1029 .unwrap();
1030 let _p2 = bucket
1031 .acquire(&ErrorKind::ServerError, &*TIME_SOURCE)
1032 .unwrap();
1033
1034 assert!(bucket.is_empty());
1035
1036 bucket.reward_success();
1039 bucket.reward_success();
1040 bucket.reward_success();
1041
1042 assert!(bucket.is_full());
1045 assert!(!bucket.is_empty());
1046 }
1047
1048 #[test]
1049 fn test_is_empty_accounts_for_fractional_tokens() {
1050 let bucket = TokenBucket::builder()
1051 .capacity(10)
1052 .retry_cost(10)
1053 .success_reward(0.5)
1054 .build();
1055
1056 let _p = bucket
1057 .acquire(&ErrorKind::ServerError, &*TIME_SOURCE)
1058 .unwrap();
1059 assert_eq!(bucket.semaphore.available_permits(), 0);
1060
1061 bucket.reward_success();
1063 assert!(bucket.is_empty());
1064
1065 bucket.reward_success();
1067 assert!(!bucket.is_empty());
1068 }
1069}