python-botocore/tests/unit/retries/test_adaptive.py
2020-03-16 19:51:55 -07:00

173 lines
6.5 KiB
Python

from tests import unittest
import mock
from botocore.retries import adaptive
from botocore.retries import standard
from botocore.retries import bucket
from botocore.retries import throttling
class FakeClock(bucket.Clock):
def __init__(self, timestamp_sequences):
self.timestamp_sequences = timestamp_sequences
self.sleep_call_amounts = []
def sleep(self, amount):
self.sleep_call_amounts.append(amount)
def current_time(self):
return self.timestamp_sequences.pop(0)
class TestCanCreateRetryHandler(unittest.TestCase):
def test_can_register_retry_handler(self):
client = mock.Mock()
limiter = adaptive.register_retry_handler(client)
self.assertEqual(
client.meta.events.register.call_args_list,
[mock.call('before-send', limiter.on_sending_request),
mock.call('needs-retry', limiter.on_receiving_response)]
)
class TestClientRateLimiter(unittest.TestCase):
def setUp(self):
self.timestamp_sequences = [0]
self.clock = FakeClock(self.timestamp_sequences)
self.token_bucket = mock.Mock(spec=bucket.TokenBucket)
self.rate_adjustor = mock.Mock(spec=throttling.CubicCalculator)
self.rate_clocker = mock.Mock(spec=adaptive.RateClocker)
self.throttling_detector = mock.Mock(
spec=standard.ThrottlingErrorDetector)
def create_client_limiter(self):
rate_limiter = adaptive.ClientRateLimiter(
rate_adjustor=self.rate_adjustor,
rate_clocker=self.rate_clocker,
token_bucket=self.token_bucket,
throttling_detector=self.throttling_detector,
clock=self.clock,
)
return rate_limiter
def test_bucket_bucket_acquisition_only_if_enabled(self):
rate_limiter = self.create_client_limiter()
rate_limiter.on_sending_request(request=mock.sentinel.request)
self.assertFalse(self.token_bucket.acquire.called)
def test_token_bucket_enabled_on_throttling_error(self):
rate_limiter = self.create_client_limiter()
self.throttling_detector.is_throttling_error.return_value = True
self.rate_clocker.record.return_value = 21
self.rate_adjustor.error_received.return_value = 17
rate_limiter.on_receiving_response()
# Now if we call on_receiving_response we should try to acquire
# token.
self.timestamp_sequences.append(1)
rate_limiter.on_sending_request(request=mock.sentinel.request)
self.assertTrue(self.token_bucket.acquire.called)
def test_max_rate_updated_on_success_response(self):
rate_limiter = self.create_client_limiter()
self.throttling_detector.is_throttling_error.return_value = False
self.rate_adjustor.success_received.return_value = 20
self.rate_clocker.record.return_value = 21
rate_limiter.on_receiving_response()
self.assertEqual(self.token_bucket.max_rate, 20)
def test_max_rate_cant_exceed_20_percent_max(self):
rate_limiter = self.create_client_limiter()
self.throttling_detector.is_throttling_error.return_value = False
# So if our actual measured sending rate is 20 TPS
self.rate_clocker.record.return_value = 20
# But the rate adjustor is telling us to go up to 100 TPS
self.rate_adjustor.success_received.return_value = 100
# The most we should go up is 2.0 * 20
rate_limiter.on_receiving_response()
self.assertEqual(self.token_bucket.max_rate, 2.0 * 20)
class TestRateClocker(unittest.TestCase):
def setUp(self):
self.timestamp_sequences = [0]
self.clock = FakeClock(self.timestamp_sequences)
self.rate_measure = adaptive.RateClocker(self.clock)
self.smoothing = 0.8
def test_initial_rate_is_0(self):
self.assertEqual(self.rate_measure.measured_rate, 0)
def test_time_updates_if_after_bucket_range(self):
self.timestamp_sequences.append(1)
# This should be 1 * 0.8 + 0 * 0.2, or just 0.8
self.assertEqual(self.rate_measure.record(), 0.8)
def test_can_measure_constant_rate(self):
# Timestamps of 1 every second indicate a rate of 1 TPS.
self.timestamp_sequences.extend(range(1, 21))
for _ in range(20):
self.rate_measure.record()
self.assertAlmostEqual(self.rate_measure.measured_rate, 1)
def test_uses_smoothing_to_favor_recent_weights(self):
self.timestamp_sequences.extend([
1,
1.5,
2,
2.5,
3,
3.5,
4,
# If we now wait 10 seconds (.1 TPS),
# our rate is somewhere between 2 TPS and .1 TPS.
14,
])
for _ in range(7):
self.rate_measure.record()
# We should almost be at 2.0 but not quite.
self.assertGreaterEqual(self.rate_measure.measured_rate, 1.99)
self.assertLessEqual(self.rate_measure.measured_rate, 2.0)
# With our last recording we now drop down between 0.1 and 2
# depending on our smoothing factor.
self.rate_measure.record()
self.assertGreaterEqual(self.rate_measure.measured_rate, 0.1)
self.assertLessEqual(self.rate_measure.measured_rate, 2.0)
def test_noop_when_delta_t_is_0(self):
self.timestamp_sequences.extend([
1,
1,
1,
2,
3
])
for _ in range(5):
self.rate_measure.record()
self.assertGreaterEqual(self.rate_measure.measured_rate, 1.0)
def test_times_are_grouped_per_time_bucket(self):
# Using our default of 0.5 time buckets, we have:
self.timestamp_sequences.extend([
0.1,
0.2,
0.3,
0.4,
0.49,
])
for _ in range(len(self.timestamp_sequences)):
self.rate_measure.record()
# This is showing the tradeoff we're making with measuring rates.
# we're currently in the window from 0 <= x < 0.5, which means
# we use the rate from the previous bucket, which is 0:
self.assertEqual(self.rate_measure.measured_rate, 0)
# However if we now add a new measurement that's in the next
# time bucket 0.5 <= x < 1.0
# we'll use the range from the previous bucket:
self.timestamp_sequences.append(0.5)
self.rate_measure.record()
# And our previous bucket will be:
# 12 * 0.8 + 0.2 * 0
self.assertEqual(self.rate_measure.measured_rate, 12 * 0.8)