Skip to main content

aws_smithy_runtime/client/retries/
client_rate_limiter.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! A rate limiter for controlling the rate at which AWS requests are made. The rate changes based
7//! on the number of throttling errors encountered.
8
9#![allow(dead_code)]
10
11use crate::client::retries::RetryPartition;
12use std::sync::{Arc, Mutex};
13use std::time::Duration;
14use tracing::debug;
15
16/// Represents a partition for the rate limiter, e.g. an endpoint, a region
17#[non_exhaustive]
18#[derive(Clone, Debug, Hash, PartialEq, Eq)]
19pub struct ClientRateLimiterPartition {
20    retry_partition: RetryPartition,
21}
22
23impl ClientRateLimiterPartition {
24    /// Creates a `ClientRateLimiterPartition` from the given [`RetryPartition`]
25    pub fn new(retry_partition: RetryPartition) -> Self {
26        Self { retry_partition }
27    }
28}
29
30const RETRY_COST: f64 = 5.0;
31const RETRY_TIMEOUT_COST: f64 = RETRY_COST * 2.0;
32const INITIAL_REQUEST_COST: f64 = 1.0;
33
34const MIN_FILL_RATE: f64 = 0.5;
35const MIN_CAPACITY: f64 = 1.0;
36const SMOOTH: f64 = 0.8;
37/// How much to scale back after receiving a throttling response
38const BETA: f64 = 0.7;
39/// Controls how aggressively we scale up after being throttled
40const SCALE_CONSTANT: f64 = 0.4;
41
42/// Rate limiter for adaptive retry.
43#[derive(Clone, Debug)]
44pub struct ClientRateLimiter {
45    pub(crate) inner: Arc<Mutex<Inner>>,
46}
47
48#[derive(Debug)]
49pub(crate) struct Inner {
50    /// The rate at which token are replenished.
51    fill_rate: f64,
52    /// The maximum capacity allowed in the token bucket.
53    max_capacity: f64,
54    /// The current capacity of the token bucket. The minimum this can be is 0.0.
55    current_capacity: f64,
56    /// The last time the token bucket was refilled.
57    last_timestamp: Option<f64>,
58    /// Boolean indicating if the token bucket is enabled.
59    /// The token bucket is initially disabled.
60    /// When a throttling error is encountered it is enabled.
61    enabled: bool,
62    /// The smoothed rate which tokens are being retrieved.
63    measured_tx_rate: f64,
64    /// The last half second time bucket used.
65    last_tx_rate_bucket: f64,
66    /// The number of requests seen within the current time bucket.
67    request_count: u64,
68    /// The maximum rate when the client was last throttled.
69    last_max_rate: f64,
70    /// The last time when the client was throttled.
71    time_of_last_throttle: f64,
72}
73
74pub(crate) enum RequestReason {
75    Retry,
76    RetryTimeout,
77    InitialRequest,
78}
79
80impl Default for ClientRateLimiter {
81    fn default() -> Self {
82        Self::builder().build()
83    }
84}
85
86impl ClientRateLimiter {
87    /// Creates a new `ClientRateLimiter`
88    pub fn new(seconds_since_unix_epoch: f64) -> Self {
89        Self::builder()
90            .tokens_retrieved_per_second(MIN_FILL_RATE)
91            .time_of_last_throttle(seconds_since_unix_epoch)
92            .previous_time_bucket(seconds_since_unix_epoch.floor())
93            .build()
94    }
95
96    /// Creates a new `ClientRateLimiterBuilder`
97    pub fn builder() -> ClientRateLimiterBuilder {
98        ClientRateLimiterBuilder::new()
99    }
100
101    pub(crate) fn acquire_permission_to_send_a_request(
102        &self,
103        seconds_since_unix_epoch: f64,
104        kind: RequestReason,
105    ) -> Result<(), Duration> {
106        let mut it = self.inner.lock().unwrap();
107
108        if !it.enabled {
109            // return early if we haven't encountered a throttling error yet
110            return Ok(());
111        }
112        let amount = match kind {
113            RequestReason::Retry => RETRY_COST,
114            RequestReason::RetryTimeout => RETRY_TIMEOUT_COST,
115            RequestReason::InitialRequest => INITIAL_REQUEST_COST,
116        };
117
118        it.refill(seconds_since_unix_epoch);
119
120        if amount > it.current_capacity {
121            let sleep_time = (amount - it.current_capacity) / it.fill_rate;
122            let dur = Duration::from_secs_f64(sleep_time);
123            if dur.is_zero() {
124                // Shortfall is sub-nanosecond (float boundary). Returning Err(0ns)
125                // makes the orchestrator's re-acquire loop spin: sleep(0) never
126                // advances the clock, so capacity never grows and we'd re-check
127                // forever. Treat it as available and grant, clamping to keep
128                // capacity >= 0 (preserving the never-negative invariant).
129                it.current_capacity = (it.current_capacity - amount).max(0.0);
130                Ok(())
131            } else {
132                debug!(
133                    amount,
134                    it.current_capacity,
135                    it.fill_rate,
136                    sleep_time,
137                    "client rate limiter delayed a request"
138                );
139                // Capacity unchanged; caller sleeps and re-acquires.
140                Err(dur)
141            }
142        } else {
143            it.current_capacity -= amount;
144            Ok(())
145        }
146    }
147
148    pub(crate) fn update_rate_limiter(
149        &self,
150        seconds_since_unix_epoch: f64,
151        is_throttling_error: bool,
152    ) {
153        let mut it = self.inner.lock().unwrap();
154        it.update_tokens_retrieved_per_second(seconds_since_unix_epoch);
155
156        let calculated_rate;
157        if is_throttling_error {
158            let rate_to_use = if it.enabled {
159                f64::min(it.measured_tx_rate, it.fill_rate)
160            } else {
161                it.measured_tx_rate
162            };
163
164            // The fill_rate is from the token bucket
165            it.last_max_rate = rate_to_use;
166            it.calculate_time_window();
167            it.time_of_last_throttle = seconds_since_unix_epoch;
168            calculated_rate = cubic_throttle(rate_to_use);
169            it.enable_token_bucket();
170        } else {
171            it.calculate_time_window();
172            calculated_rate = it.cubic_success(seconds_since_unix_epoch);
173        }
174
175        let new_rate = f64::min(calculated_rate, 2.0 * it.measured_tx_rate);
176        it.update_bucket_refill_rate(seconds_since_unix_epoch, new_rate);
177    }
178}
179
180impl Inner {
181    fn refill(&mut self, seconds_since_unix_epoch: f64) {
182        if let Some(last_timestamp) = self.last_timestamp {
183            let fill_amount = (seconds_since_unix_epoch - last_timestamp) * self.fill_rate;
184            self.current_capacity =
185                f64::min(self.max_capacity, self.current_capacity + fill_amount);
186            debug!(
187                fill_amount,
188                self.current_capacity, self.max_capacity, "refilling client rate limiter tokens"
189            );
190        }
191        self.last_timestamp = Some(seconds_since_unix_epoch);
192    }
193
194    fn update_bucket_refill_rate(&mut self, seconds_since_unix_epoch: f64, new_fill_rate: f64) {
195        // Refill based on our current rate before we update to the new fill rate.
196        self.refill(seconds_since_unix_epoch);
197
198        self.fill_rate = f64::max(new_fill_rate, MIN_FILL_RATE);
199        self.max_capacity = f64::max(new_fill_rate, MIN_CAPACITY);
200
201        debug!(
202            fill_rate = self.fill_rate,
203            max_capacity = self.max_capacity,
204            current_capacity = self.current_capacity,
205            measured_tx_rate = self.measured_tx_rate,
206            "client rate limiter state has been updated"
207        );
208
209        // When we scale down we can't have a current capacity that exceeds our max_capacity.
210        self.current_capacity = f64::min(self.current_capacity, self.max_capacity);
211    }
212
213    fn enable_token_bucket(&mut self) {
214        // If throttling wasn't already enabled, note that we're now enabling it.
215        if !self.enabled {
216            debug!("client rate limiting has been enabled");
217        }
218        self.enabled = true;
219    }
220
221    fn update_tokens_retrieved_per_second(&mut self, seconds_since_unix_epoch: f64) {
222        let next_time_bucket = (seconds_since_unix_epoch * 2.0).floor() / 2.0;
223        self.request_count += 1;
224
225        if next_time_bucket > self.last_tx_rate_bucket {
226            let current_rate =
227                self.request_count as f64 / (next_time_bucket - self.last_tx_rate_bucket);
228            self.measured_tx_rate = current_rate * SMOOTH + self.measured_tx_rate * (1.0 - SMOOTH);
229            self.request_count = 0;
230            self.last_tx_rate_bucket = next_time_bucket;
231        }
232    }
233
234    fn calculate_time_window(&self) -> f64 {
235        let base = (self.last_max_rate * (1.0 - BETA)) / SCALE_CONSTANT;
236        base.powf(1.0 / 3.0)
237    }
238
239    fn cubic_success(&self, seconds_since_unix_epoch: f64) -> f64 {
240        let dt =
241            seconds_since_unix_epoch - self.time_of_last_throttle - self.calculate_time_window();
242        (SCALE_CONSTANT * dt.powi(3)) + self.last_max_rate
243    }
244}
245
246fn cubic_throttle(rate_to_use: f64) -> f64 {
247    rate_to_use * BETA
248}
249
250/// Builder for `ClientRateLimiter`.
251#[derive(Clone, Debug, Default)]
252pub struct ClientRateLimiterBuilder {
253    ///The rate at which token are replenished.
254    token_refill_rate: Option<f64>,
255    ///The maximum capacity allowed in the token bucket.
256    maximum_bucket_capacity: Option<f64>,
257    ///The current capacity of the token bucket.
258    current_bucket_capacity: Option<f64>,
259    ///The last time the token bucket was refilled.
260    time_of_last_refill: Option<f64>,
261    ///The smoothed rate which tokens are being retrieved.
262    tokens_retrieved_per_second: Option<f64>,
263    ///The last half second time bucket used.
264    previous_time_bucket: Option<f64>,
265    ///The number of requests seen within the current time bucket.
266    request_count: Option<u64>,
267    ///Boolean indicating if the token bucket is enabled. The token bucket is initially disabled. When a throttling error is encountered it is enabled.
268    enable_throttling: Option<bool>,
269    ///The maximum rate when the client was last throttled.
270    tokens_retrieved_per_second_at_time_of_last_throttle: Option<f64>,
271    ///The last time when the client was throttled.
272    time_of_last_throttle: Option<f64>,
273}
274
275impl ClientRateLimiterBuilder {
276    /// Create a new `ClientRateLimiterBuilder`.
277    pub fn new() -> Self {
278        ClientRateLimiterBuilder::default()
279    }
280    /// The rate at which token are replenished.
281    pub fn token_refill_rate(mut self, token_refill_rate: f64) -> Self {
282        self.set_token_refill_rate(Some(token_refill_rate));
283        self
284    }
285    /// The rate at which token are replenished.
286    pub fn set_token_refill_rate(&mut self, token_refill_rate: Option<f64>) -> &mut Self {
287        self.token_refill_rate = token_refill_rate;
288        self
289    }
290    /// The maximum capacity allowed in the token bucket
291    ///
292    /// The implementation of [`ClientRateLimiter`] guarantees that `current_capacity` never exceeds this value.
293    pub fn maximum_bucket_capacity(mut self, maximum_bucket_capacity: f64) -> Self {
294        self.set_maximum_bucket_capacity(Some(maximum_bucket_capacity));
295        self
296    }
297    /// The maximum capacity allowed in the token bucket
298    ///
299    /// The implementation of [`ClientRateLimiter`] guarantees that `current_capacity` never exceeds this value.
300    pub fn set_maximum_bucket_capacity(
301        &mut self,
302        maximum_bucket_capacity: Option<f64>,
303    ) -> &mut Self {
304        self.maximum_bucket_capacity = maximum_bucket_capacity;
305        self
306    }
307    /// The current capacity of the token bucket
308    ///
309    /// The implementation of [`ClientRateLimiter`] guarantees that this value is always at least `1.0` when it's enabled.
310    pub fn current_bucket_capacity(mut self, current_bucket_capacity: f64) -> Self {
311        self.set_current_bucket_capacity(Some(current_bucket_capacity));
312        self
313    }
314    /// The current capacity of the token bucket
315    ///
316    /// The implementation of [`ClientRateLimiter`] guarantees that this value is always at least `1.0` when it's enabled.
317    pub fn set_current_bucket_capacity(
318        &mut self,
319        current_bucket_capacity: Option<f64>,
320    ) -> &mut Self {
321        self.current_bucket_capacity = current_bucket_capacity;
322        self
323    }
324    // The last time the token bucket was refilled.
325    fn time_of_last_refill(mut self, time_of_last_refill: f64) -> Self {
326        self.set_time_of_last_refill(Some(time_of_last_refill));
327        self
328    }
329    // The last time the token bucket was refilled.
330    fn set_time_of_last_refill(&mut self, time_of_last_refill: Option<f64>) -> &mut Self {
331        self.time_of_last_refill = time_of_last_refill;
332        self
333    }
334    /// The smoothed rate which tokens are being retrieved.
335    pub fn tokens_retrieved_per_second(mut self, tokens_retrieved_per_second: f64) -> Self {
336        self.set_tokens_retrieved_per_second(Some(tokens_retrieved_per_second));
337        self
338    }
339    /// The smoothed rate which tokens are being retrieved.
340    pub fn set_tokens_retrieved_per_second(
341        &mut self,
342        tokens_retrieved_per_second: Option<f64>,
343    ) -> &mut Self {
344        self.tokens_retrieved_per_second = tokens_retrieved_per_second;
345        self
346    }
347    // The last half second time bucket used.
348    fn previous_time_bucket(mut self, previous_time_bucket: f64) -> Self {
349        self.set_previous_time_bucket(Some(previous_time_bucket));
350        self
351    }
352    // The last half second time bucket used.
353    fn set_previous_time_bucket(&mut self, previous_time_bucket: Option<f64>) -> &mut Self {
354        self.previous_time_bucket = previous_time_bucket;
355        self
356    }
357    // The number of requests seen within the current time bucket.
358    fn request_count(mut self, request_count: u64) -> Self {
359        self.set_request_count(Some(request_count));
360        self
361    }
362    // The number of requests seen within the current time bucket.
363    fn set_request_count(&mut self, request_count: Option<u64>) -> &mut Self {
364        self.request_count = request_count;
365        self
366    }
367    // Boolean indicating if the token bucket is enabled. The token bucket is initially disabled. When a throttling error is encountered it is enabled.
368    fn enable_throttling(mut self, enable_throttling: bool) -> Self {
369        self.set_enable_throttling(Some(enable_throttling));
370        self
371    }
372    // Boolean indicating if the token bucket is enabled. The token bucket is initially disabled. When a throttling error is encountered it is enabled.
373    fn set_enable_throttling(&mut self, enable_throttling: Option<bool>) -> &mut Self {
374        self.enable_throttling = enable_throttling;
375        self
376    }
377    // The maximum rate when the client was last throttled.
378    fn tokens_retrieved_per_second_at_time_of_last_throttle(
379        mut self,
380        tokens_retrieved_per_second_at_time_of_last_throttle: f64,
381    ) -> Self {
382        self.set_tokens_retrieved_per_second_at_time_of_last_throttle(Some(
383            tokens_retrieved_per_second_at_time_of_last_throttle,
384        ));
385        self
386    }
387    // The maximum rate when the client was last throttled.
388    fn set_tokens_retrieved_per_second_at_time_of_last_throttle(
389        &mut self,
390        tokens_retrieved_per_second_at_time_of_last_throttle: Option<f64>,
391    ) -> &mut Self {
392        self.tokens_retrieved_per_second_at_time_of_last_throttle =
393            tokens_retrieved_per_second_at_time_of_last_throttle;
394        self
395    }
396    // The last time when the client was throttled.
397    fn time_of_last_throttle(mut self, time_of_last_throttle: f64) -> Self {
398        self.set_time_of_last_throttle(Some(time_of_last_throttle));
399        self
400    }
401    // The last time when the client was throttled.
402    fn set_time_of_last_throttle(&mut self, time_of_last_throttle: Option<f64>) -> &mut Self {
403        self.time_of_last_throttle = time_of_last_throttle;
404        self
405    }
406    /// Build the ClientRateLimiter.
407    pub fn build(self) -> ClientRateLimiter {
408        ClientRateLimiter {
409            inner: Arc::new(Mutex::new(Inner {
410                fill_rate: self.token_refill_rate.unwrap_or_default(),
411                max_capacity: self.maximum_bucket_capacity.unwrap_or(f64::MAX),
412                current_capacity: self.current_bucket_capacity.unwrap_or_default(),
413                last_timestamp: self.time_of_last_refill,
414                enabled: self.enable_throttling.unwrap_or_default(),
415                measured_tx_rate: self.tokens_retrieved_per_second.unwrap_or_default(),
416                last_tx_rate_bucket: self.previous_time_bucket.unwrap_or_default(),
417                request_count: self.request_count.unwrap_or_default(),
418                last_max_rate: self
419                    .tokens_retrieved_per_second_at_time_of_last_throttle
420                    .unwrap_or_default(),
421                time_of_last_throttle: self.time_of_last_throttle.unwrap_or_default(),
422            })),
423        }
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::{cubic_throttle, ClientRateLimiter, INITIAL_REQUEST_COST};
430    use crate::client::retries::client_rate_limiter::RequestReason;
431    use approx::assert_relative_eq;
432    use aws_smithy_async::rt::sleep::AsyncSleep;
433    use aws_smithy_async::test_util::instant_time_and_sleep;
434    use std::time::{Duration, SystemTime};
435
436    const ONE_SECOND: Duration = Duration::from_secs(1);
437    const TWO_HUNDRED_MILLISECONDS: Duration = Duration::from_millis(200);
438
439    #[test]
440    fn should_match_beta_decrease() {
441        let new_rate = cubic_throttle(10.0);
442        assert_relative_eq!(new_rate, 7.0);
443
444        let rate_limiter = ClientRateLimiter::builder()
445            .tokens_retrieved_per_second_at_time_of_last_throttle(10.0)
446            .time_of_last_throttle(1.0)
447            .build();
448
449        rate_limiter.inner.lock().unwrap().calculate_time_window();
450        let new_rate = rate_limiter.inner.lock().unwrap().cubic_success(1.0);
451        assert_relative_eq!(new_rate, 7.0);
452    }
453
454    #[tokio::test]
455    async fn throttling_is_enabled_once_throttling_error_is_received() {
456        let rate_limiter = ClientRateLimiter::builder()
457            .previous_time_bucket(0.0)
458            .time_of_last_throttle(0.0)
459            .build();
460
461        assert!(
462            !rate_limiter.inner.lock().unwrap().enabled,
463            "rate_limiter should be disabled by default"
464        );
465        rate_limiter.update_rate_limiter(0.0, true);
466        assert!(
467            rate_limiter.inner.lock().unwrap().enabled,
468            "rate_limiter should be enabled after throttling error"
469        );
470    }
471
472    #[tokio::test]
473    async fn test_calculated_rate_with_successes() {
474        let rate_limiter = ClientRateLimiter::builder()
475            .time_of_last_throttle(5.0)
476            .tokens_retrieved_per_second_at_time_of_last_throttle(10.0)
477            .build();
478
479        struct Attempt {
480            seconds_since_unix_epoch: f64,
481            expected_calculated_rate: f64,
482        }
483
484        let attempts = [
485            Attempt {
486                seconds_since_unix_epoch: 5.0,
487                expected_calculated_rate: 7.0,
488            },
489            Attempt {
490                seconds_since_unix_epoch: 6.0,
491                expected_calculated_rate: 9.64893600966,
492            },
493            Attempt {
494                seconds_since_unix_epoch: 7.0,
495                expected_calculated_rate: 10.000030849917364,
496            },
497            Attempt {
498                seconds_since_unix_epoch: 8.0,
499                expected_calculated_rate: 10.453284520772092,
500            },
501            Attempt {
502                seconds_since_unix_epoch: 9.0,
503                expected_calculated_rate: 13.408697022224185,
504            },
505            Attempt {
506                seconds_since_unix_epoch: 10.0,
507                expected_calculated_rate: 21.26626835427364,
508            },
509            Attempt {
510                seconds_since_unix_epoch: 11.0,
511                expected_calculated_rate: 36.425998516920465,
512            },
513        ];
514
515        // Think this test is a little strange? I ported the test from Go v2, and this is how it
516        // was implemented. See for yourself:
517        // https://github.com/aws/aws-sdk-go-v2/blob/844ff45cdc76182229ad098c95bf3f5ab8c20e9f/aws/retry/adaptive_ratelimit_test.go#L97
518        for attempt in attempts {
519            rate_limiter.inner.lock().unwrap().calculate_time_window();
520            let calculated_rate = rate_limiter
521                .inner
522                .lock()
523                .unwrap()
524                .cubic_success(attempt.seconds_since_unix_epoch);
525
526            assert_relative_eq!(attempt.expected_calculated_rate, calculated_rate);
527        }
528    }
529
530    #[tokio::test]
531    async fn test_calculated_rate_with_throttles() {
532        let rate_limiter = ClientRateLimiter::builder()
533            .tokens_retrieved_per_second_at_time_of_last_throttle(10.0)
534            .time_of_last_throttle(5.0)
535            .build();
536
537        struct Attempt {
538            throttled: bool,
539            seconds_since_unix_epoch: f64,
540            expected_calculated_rate: f64,
541        }
542
543        let attempts = [
544            Attempt {
545                throttled: false,
546                seconds_since_unix_epoch: 5.0,
547                expected_calculated_rate: 7.0,
548            },
549            Attempt {
550                throttled: false,
551                seconds_since_unix_epoch: 6.0,
552                expected_calculated_rate: 9.64893600966,
553            },
554            Attempt {
555                throttled: true,
556                seconds_since_unix_epoch: 7.0,
557                expected_calculated_rate: 6.754255206761999,
558            },
559            Attempt {
560                throttled: true,
561                seconds_since_unix_epoch: 8.0,
562                expected_calculated_rate: 4.727978644733399,
563            },
564            Attempt {
565                throttled: false,
566                seconds_since_unix_epoch: 9.0,
567                expected_calculated_rate: 4.670125557970046,
568            },
569            Attempt {
570                throttled: false,
571                seconds_since_unix_epoch: 10.0,
572                expected_calculated_rate: 4.770870456867401,
573            },
574            Attempt {
575                throttled: false,
576                seconds_since_unix_epoch: 11.0,
577                expected_calculated_rate: 6.011819748005445,
578            },
579            Attempt {
580                throttled: false,
581                seconds_since_unix_epoch: 12.0,
582                expected_calculated_rate: 10.792973431384178,
583            },
584        ];
585
586        // Think this test is a little strange? I ported the test from Go v2, and this is how it
587        // was implemented. See for yourself:
588        // https://github.com/aws/aws-sdk-go-v2/blob/844ff45cdc76182229ad098c95bf3f5ab8c20e9f/aws/retry/adaptive_ratelimit_test.go#L97
589        let mut calculated_rate = 0.0;
590        for attempt in attempts {
591            let mut inner = rate_limiter.inner.lock().unwrap();
592            inner.calculate_time_window();
593            if attempt.throttled {
594                calculated_rate = cubic_throttle(calculated_rate);
595                inner.time_of_last_throttle = attempt.seconds_since_unix_epoch;
596                inner.last_max_rate = calculated_rate;
597            } else {
598                calculated_rate = inner.cubic_success(attempt.seconds_since_unix_epoch);
599            };
600
601            assert_relative_eq!(attempt.expected_calculated_rate, calculated_rate);
602        }
603    }
604
605    #[tokio::test]
606    async fn test_client_sending_rates() {
607        let (_, sleep_impl) = instant_time_and_sleep(SystemTime::UNIX_EPOCH);
608        let rate_limiter = ClientRateLimiter::builder().build();
609
610        struct Attempt {
611            throttled: bool,
612            seconds_since_unix_epoch: f64,
613            expected_tokens_retrieved_per_second: f64,
614            expected_token_refill_rate: f64,
615        }
616
617        let attempts = [
618            Attempt {
619                throttled: false,
620                seconds_since_unix_epoch: 0.2,
621                expected_tokens_retrieved_per_second: 0.000000,
622                expected_token_refill_rate: 0.500000,
623            },
624            Attempt {
625                throttled: false,
626                seconds_since_unix_epoch: 0.4,
627                expected_tokens_retrieved_per_second: 0.000000,
628                expected_token_refill_rate: 0.500000,
629            },
630            Attempt {
631                throttled: false,
632                seconds_since_unix_epoch: 0.6,
633                expected_tokens_retrieved_per_second: 4.800000000000001,
634                expected_token_refill_rate: 0.500000,
635            },
636            Attempt {
637                throttled: false,
638                seconds_since_unix_epoch: 0.8,
639                expected_tokens_retrieved_per_second: 4.800000000000001,
640                expected_token_refill_rate: 0.500000,
641            },
642            Attempt {
643                throttled: false,
644                seconds_since_unix_epoch: 1.0,
645                expected_tokens_retrieved_per_second: 4.160000,
646                expected_token_refill_rate: 0.500000,
647            },
648            Attempt {
649                throttled: false,
650                seconds_since_unix_epoch: 1.2,
651                expected_tokens_retrieved_per_second: 4.160000,
652                expected_token_refill_rate: 0.691200,
653            },
654            Attempt {
655                throttled: false,
656                seconds_since_unix_epoch: 1.4,
657                expected_tokens_retrieved_per_second: 4.160000,
658                expected_token_refill_rate: 1.0975999999999997,
659            },
660            Attempt {
661                throttled: false,
662                seconds_since_unix_epoch: 1.6,
663                expected_tokens_retrieved_per_second: 5.632000000000001,
664                expected_token_refill_rate: 1.6384000000000005,
665            },
666            Attempt {
667                throttled: false,
668                seconds_since_unix_epoch: 1.8,
669                expected_tokens_retrieved_per_second: 5.632000000000001,
670                expected_token_refill_rate: 2.332800,
671            },
672            Attempt {
673                throttled: true,
674                seconds_since_unix_epoch: 2.0,
675                expected_tokens_retrieved_per_second: 4.326400,
676                expected_token_refill_rate: 3.0284799999999996,
677            },
678            Attempt {
679                throttled: false,
680                seconds_since_unix_epoch: 2.2,
681                expected_tokens_retrieved_per_second: 4.326400,
682                expected_token_refill_rate: 3.48663917347026,
683            },
684            Attempt {
685                throttled: false,
686                seconds_since_unix_epoch: 2.4,
687                expected_tokens_retrieved_per_second: 4.326400,
688                expected_token_refill_rate: 3.821874416040255,
689            },
690            Attempt {
691                throttled: false,
692                seconds_since_unix_epoch: 2.6,
693                expected_tokens_retrieved_per_second: 5.665280,
694                expected_token_refill_rate: 4.053385727709987,
695            },
696            Attempt {
697                throttled: false,
698                seconds_since_unix_epoch: 2.8,
699                expected_tokens_retrieved_per_second: 5.665280,
700                expected_token_refill_rate: 4.200373108479454,
701            },
702            Attempt {
703                throttled: false,
704                seconds_since_unix_epoch: 3.0,
705                expected_tokens_retrieved_per_second: 4.333056,
706                expected_token_refill_rate: 4.282036558348658,
707            },
708            Attempt {
709                throttled: true,
710                seconds_since_unix_epoch: 3.2,
711                expected_tokens_retrieved_per_second: 4.333056,
712                expected_token_refill_rate: 2.99742559084406,
713            },
714            Attempt {
715                throttled: false,
716                seconds_since_unix_epoch: 3.4,
717                expected_tokens_retrieved_per_second: 4.333056,
718                expected_token_refill_rate: 3.4522263943863463,
719            },
720        ];
721
722        for attempt in attempts {
723            sleep_impl.sleep(TWO_HUNDRED_MILLISECONDS).await;
724            assert_eq!(
725                attempt.seconds_since_unix_epoch,
726                sleep_impl.total_duration().as_secs_f64()
727            );
728
729            rate_limiter.update_rate_limiter(attempt.seconds_since_unix_epoch, attempt.throttled);
730            assert_relative_eq!(
731                attempt.expected_tokens_retrieved_per_second,
732                rate_limiter.inner.lock().unwrap().measured_tx_rate
733            );
734            assert_relative_eq!(
735                attempt.expected_token_refill_rate,
736                rate_limiter.inner.lock().unwrap().fill_rate
737            );
738        }
739    }
740
741    // Regression test for the multi-thread negative capacity bug.
742    // See: https://github.com/smithy-lang/smithy-rs/blob/main/rust-runtime/aws-smithy-runtime/src/client/retries/client_rate_limiter.rs#L135
743    // Failing test showing the bug: https://github.com/smithy-lang/smithy-rs/commit/786b6d07e17d39ae0a6c040a49664169149c2fdf
744    //
745    // Previously, capacity was deducted unconditionally (even on Err),
746    // allowing it to go to -0.95 in this scenario. Now capacity is only
747    // deducted when a token is actually granted (Ok), so it stays at 0.05.
748    #[tokio::test]
749    async fn test_capacity_never_goes_negative() {
750        let rate_limiter = ClientRateLimiter::builder()
751            .time_of_last_throttle(0.0)
752            .previous_time_bucket(0.0)
753            .build();
754
755        rate_limiter.update_rate_limiter(0.0, true);
756
757        // fill_rate = 0.5, 0.1s elapsed => refill adds 0.05 tokens.
758        // Cost of InitialRequest is 1.0, so capacity (0.05) is insufficient.
759        let result =
760            rate_limiter.acquire_permission_to_send_a_request(0.1, RequestReason::InitialRequest);
761
762        assert!(result.is_err(), "should require waiting for capacity");
763
764        let inner = rate_limiter.inner.lock().unwrap();
765        assert_relative_eq!(inner.current_capacity, 0.05, epsilon = 0.01);
766        assert_relative_eq!(result.unwrap_err().as_secs_f64(), 1.9, epsilon = 0.01);
767    }
768
769    #[tokio::test]
770    async fn test_concurrent_acquires_no_cascading_delays() {
771        let rate_limiter = ClientRateLimiter::builder()
772            .time_of_last_throttle(0.0)
773            .previous_time_bucket(0.0)
774            .build();
775
776        rate_limiter.update_rate_limiter(0.0, true);
777
778        let mut delays = Vec::with_capacity(10);
779        for _ in 0..10 {
780            let result = rate_limiter
781                .acquire_permission_to_send_a_request(0.1, RequestReason::InitialRequest);
782            assert!(result.is_err());
783            delays.push(result.unwrap_err());
784        }
785
786        // All delays must be identical — no cascading
787        let first = delays[0];
788        for delay in &delays {
789            assert_eq!(*delay, first, "all tasks should get the same delay");
790        }
791
792        let inner = rate_limiter.inner.lock().unwrap();
793        assert!(
794            inner.current_capacity >= 0.0,
795            "capacity must never be negative"
796        );
797    }
798
799    #[tokio::test]
800    async fn test_acquire_succeeds_after_sufficient_refill() {
801        let rate_limiter = ClientRateLimiter::builder()
802            .time_of_last_throttle(0.0)
803            .previous_time_bucket(0.0)
804            .build();
805
806        rate_limiter.update_rate_limiter(0.0, true);
807
808        let result =
809            rate_limiter.acquire_permission_to_send_a_request(0.1, RequestReason::InitialRequest);
810        assert!(result.is_err());
811
812        // At 2.1s: refill adds 1.0 token, capped at max_capacity (1.0).
813        let result =
814            rate_limiter.acquire_permission_to_send_a_request(2.1, RequestReason::InitialRequest);
815        assert!(result.is_ok(), "should succeed after sufficient refill");
816
817        let inner = rate_limiter.inner.lock().unwrap();
818        assert_relative_eq!(inner.current_capacity, 0.0, epsilon = 0.01);
819    }
820
821    #[tokio::test]
822    async fn test_sub_nanosecond_shortfall_grants_instead_of_returning_zero_delay() {
823        let rate_limiter = ClientRateLimiter::builder()
824            .time_of_last_throttle(0.0)
825            .previous_time_bucket(0.0)
826            .build();
827        rate_limiter.update_rate_limiter(0.0, true); // enable throttling
828
829        // Force capacity infinitesimally below the InitialRequest cost so the
830        // computed wait (shortfall / fill_rate) rounds below 1ns. Pin
831        // `last_timestamp` so the acquire below refills ~nothing.
832        {
833            let mut inner = rate_limiter.inner.lock().unwrap();
834            inner.current_capacity = INITIAL_REQUEST_COST - 1e-12;
835            inner.last_timestamp = Some(0.5);
836        }
837
838        // Previously this returned `Err(0ns)`, which spins the orchestrator's
839        // re-acquire loop (sleep(0) never advances capacity). It must now grant.
840        let result =
841            rate_limiter.acquire_permission_to_send_a_request(0.5, RequestReason::InitialRequest);
842        assert!(
843            result.is_ok(),
844            "a sub-nanosecond shortfall must grant, not return Err(0ns)"
845        );
846
847        // Capacity must remain non-negative (the #4632 invariant).
848        let inner = rate_limiter.inner.lock().unwrap();
849        assert!(
850            inner.current_capacity >= 0.0,
851            "capacity must not go negative, was {}",
852            inner.current_capacity
853        );
854    }
855
856    // This test is only testing that we don't fail basic math and panic. It does include an
857    // element of randomness, but no duration between >= 0.0s and <= 1.0s will ever cause a panic.
858    //
859    // Because the cost of sending an individual request is 1.0, and because the minimum capacity is
860    // also 1.0, we will never encounter a situation where we run out of tokens.
861    #[tokio::test]
862    async fn test_when_throttling_is_enabled_requests_can_still_be_sent() {
863        let (time_source, sleep_impl) = instant_time_and_sleep(SystemTime::UNIX_EPOCH);
864        let crl = ClientRateLimiter::builder()
865            .time_of_last_throttle(0.0)
866            .previous_time_bucket(0.0)
867            .build();
868
869        // Start by recording a throttling error
870        crl.update_rate_limiter(0.0, true);
871
872        for _i in 0..100 {
873            // advance time by a random amount (up to 1s) each iteration
874            let duration = Duration::from_secs_f64(fastrand::f64());
875            sleep_impl.sleep(duration).await;
876            if let Err(delay) = crl.acquire_permission_to_send_a_request(
877                time_source.seconds_since_unix_epoch(),
878                RequestReason::InitialRequest,
879            ) {
880                sleep_impl.sleep(delay).await;
881            }
882
883            // Assume all further requests succeed on the first try
884            crl.update_rate_limiter(time_source.seconds_since_unix_epoch(), false);
885        }
886
887        let inner = crl.inner.lock().unwrap();
888        assert!(inner.enabled, "the rate limiter should still be enabled");
889        // Assert that the rate limiter respects the passage of time.
890        assert_relative_eq!(
891            inner.last_timestamp.unwrap(),
892            sleep_impl.total_duration().as_secs_f64(),
893            max_relative = 0.0001
894        );
895    }
896
897    #[tokio::test]
898    async fn test_multi_task_recovery_after_throttle_blip() {
899        // Simulates the transient throttle blip scenario: 50 tasks share a
900        // rate limiter, all get throttled, then throttle lifts. Verifies
901        // that tasks recover and can acquire tokens again.
902        let crl = ClientRateLimiter::builder()
903            .time_of_last_throttle(0.0)
904            .previous_time_bucket(0.0)
905            .build();
906
907        let num_tasks = 50;
908        let mut time = 0.0;
909
910        // All tasks get throttled
911        for _ in 0..num_tasks {
912            time += 0.001;
913            crl.update_rate_limiter(time, true);
914        }
915
916        assert_relative_eq!(crl.inner.lock().unwrap().fill_rate, 0.5, epsilon = 0.01);
917
918        // Simulate recovery over 20 seconds (200 rounds * 100ms).
919        // Tasks that acquire successfully get a success response,
920        // which increases the fill rate.
921        let mut total_acquired = 0;
922        let rounds = 200;
923        for _ in 0..rounds {
924            time += 0.1;
925            let mut acquired_this_round = 0;
926
927            for task in 0..num_tasks {
928                let task_time = time + (task as f64) * 0.0001;
929                if crl
930                    .acquire_permission_to_send_a_request(task_time, RequestReason::InitialRequest)
931                    .is_ok()
932                {
933                    acquired_this_round += 1;
934                }
935            }
936
937            for _ in 0..acquired_this_round {
938                time += 0.001;
939                crl.update_rate_limiter(time, false);
940            }
941
942            total_acquired += acquired_this_round;
943        }
944
945        assert!(
946            total_acquired > 100,
947            "expected recovery after throttle blip, but only acquired {total_acquired} tokens in {rounds} rounds"
948        );
949
950        let inner = crl.inner.lock().unwrap();
951        assert!(
952            inner.current_capacity >= 0.0,
953            "capacity must never be negative, got: {}",
954            inner.current_capacity
955        );
956    }
957}