class RateLimiter: def __init__(self, storage, limit, *, identifiers=None, metrics): if identifiers is None: identifiers = [] self._window = MovingWindowRateLimiter(storage) self._limits = parse_many(limit) self._identifiers = identifiers self._metrics = metrics def _get_identifiers(self, identifiers): return [str(i) for i in list(self._identifiers) + list(identifiers)] @_return_on_exception(True, redis.RedisError) def test(self, *identifiers): return all( [ self._window.test(limit, *self._get_identifiers(identifiers)) for limit in self._limits ] ) @_return_on_exception(True, redis.RedisError) def hit(self, *identifiers): return all( [ self._window.hit(limit, *self._get_identifiers(identifiers)) for limit in self._limits ] ) @_return_on_exception(None, redis.RedisError) def resets_in(self, *identifiers): resets = [] for limit in self._limits: resets_at, remaining = self._window.get_window_stats( limit, *self._get_identifiers(identifiers) ) # If this limit has any remaining limits left, then we will skip it # since it doesn't need reset. if remaining > 0: continue current = datetime.now(tz=timezone.utc) reset = datetime.fromtimestamp(resets_at, tz=timezone.utc) # If our current datetime is either greater than or equal to when # the limit resets, then we will skipp it since it has either # already reset, or it is resetting now. if current >= reset: continue # Add a timedelta that represents how long until this limit resets. resets.append(reset - current) # If we have any resets, then we'll go through and find whichever one # is going to reset soonest and use that as our hint for when this # limit might be available again. return first(sorted(resets))
def __init__(self, storage, limit, identifiers=None): if identifiers is None: identifiers = [] self._window = MovingWindowRateLimiter(storage) self._limits = parse_many(limit) self._identifiers = identifiers
class RateLimiter: def __init__(self, storage, limit, identifiers=None): if identifiers is None: identifiers = [] self._window = MovingWindowRateLimiter(storage) self._limits = parse_many(limit) self._identifiers = identifiers def _get_identifiers(self, identifiers): return [str(i) for i in list(self._identifiers) + list(identifiers)] def test(self, *identifiers): return all( [ self._window.test(limit, *self._get_identifiers(identifiers)) for limit in self._limits ] ) def hit(self, *identifiers): return all( [ self._window.hit(limit, *self._get_identifiers(identifiers)) for limit in self._limits ] ) def resets_in(self, *identifiers): resets = [] for limit in self._limits: resets_at, remaining = self._window.get_window_stats( limit, *self._get_identifiers(identifiers) ) # If this limit has any remaining limits left, then we will skip it # since it doesn't need reset. if remaining > 0: continue current = datetime.now(tz=timezone.utc) reset = datetime.fromtimestamp(resets_at, tz=timezone.utc) # If our current datetime is either greater than or equal to when # the limit resets, then we will skipp it since it has either # already reset, or it is resetting now. if current >= reset: continue # Add a timedelta that represents how long until this limit resets. resets.append(reset - current) # If we have any resets, then we'll go through and find whichever one # is going to reset soonest and use that as our hint for when this # limit might be available again. return first(sorted(resets))
def __init__(self, storage, limit, *, identifiers=None, metrics): if identifiers is None: identifiers = [] self._storage = storage self._window = MovingWindowRateLimiter(storage) self._limits = parse_many(limit) self._identifiers = identifiers self._metrics = metrics
def test_in_memory_expiry_moving_window(self): with hiro.Timeline().freeze() as timeline: storage = MemoryStorage() limiter = MovingWindowRateLimiter(storage) per_min = RateLimitItemPerMinute(10) per_sec = RateLimitItemPerSecond(1) for i in range(0, 2): for i in range(0, 10): self.assertTrue(limiter.hit(per_min)) timeline.forward(60) self.assertTrue(limiter.hit(per_sec)) time.sleep(1) self.assertEqual([], storage.events[per_min.key_for()])
def test_in_memory_expiry_moving_window(self): with hiro.Timeline().freeze() as timeline: storage = MemoryStorage() limiter = MovingWindowRateLimiter(storage) per_min = RateLimitItemPerMinute(10) per_sec = RateLimitItemPerSecond(1) for i in range(0,2): for i in range(0,10): self.assertTrue(limiter.hit(per_min)) timeline.forward(60) self.assertTrue(limiter.hit(per_sec)) time.sleep(1) self.assertEqual([], storage.events[per_min.key_for()])
def test_moving_window_clear(self): limiter = MovingWindowRateLimiter(self.storage) per_min = RateLimitItemPerMinute(1) limiter.hit(per_min) self.assertFalse(limiter.hit(per_min)) limiter.clear(per_min) self.assertTrue(limiter.hit(per_min))
def test_redis_cluster_clear(self): storage = RedisClusterStorage("redis+cluster://localhost:7000") limiter = MovingWindowRateLimiter(storage) per_min = RateLimitItemPerMinute(1) limiter.hit(per_min) self.assertFalse(limiter.hit(per_min)) limiter.clear(per_min) self.assertTrue(limiter.hit(per_min))
def test_large_dataset_redis_sentinel_moving_window_expiry(self): storage = RedisSentinelStorage("redis+sentinel://localhost:26379", service_name="localhost-redis-sentinel") limiter = MovingWindowRateLimiter(storage) limit = RateLimitItemPerSecond(1000) keys_start = storage.sentinel.slave_for( "localhost-redis-sentinel").keys("%s/*" % limit.namespace) # 100 routes fake_routes = [uuid4().hex for _ in range(0, 100)] # go as fast as possible in 2 seconds. start = time.time() def smack(e): while not e.is_set(): self.assertTrue(limiter.hit(limit, random.choice(fake_routes))) events = [threading.Event() for _ in range(0, 100)] threads = [threading.Thread(target=smack, args=(e, )) for e in events] [k.start() for k in threads] while time.time() - start < 2: time.sleep(0.1) [k.set() for k in events] time.sleep(2) self.assertTrue( storage.sentinel.slave_for("localhost-redis-sentinel").keys( "%s/*" % limit.namespace) == [])
def __init__(self, storage, limit, identifiers=None): if identifiers is None: identifiers = [] self._window = MovingWindowRateLimiter(storage) self._limits = parse_many(limit) self._identifiers = identifiers
def test_test_moving_window(self): with hiro.Timeline().freeze(): store = MemoryStorage() limit = RateLimitItemPerSecond(2, 1) limiter = MovingWindowRateLimiter(store) self.assertTrue(limiter.hit(limit), store) self.assertTrue(limiter.test(limit), store) self.assertTrue(limiter.hit(limit), store) self.assertFalse(limiter.test(limit), store) self.assertFalse(limiter.hit(limit), store)
def test_memory_storage_moving_window(self): storage = MemoryStorage() limiter = MovingWindowRateLimiter(storage) per_second = RateLimitItemPerSecond(100) [limiter.hit(per_second, uuid4().hex) for _ in range(100)] key = uuid4().hex hits = [] def hit(): if limiter.hit(per_second, key): hits.append(None) start = time.time() threads = [threading.Thread(target=hit) for _ in range(1000)] [t.start() for t in threads] [t.join() for t in threads] self.assertTrue(time.time() - start < 1) self.assertEqual(len(hits), 100)
def test_moving_window_in_memory(self): storage = MemoryStorage() limiter = MovingWindowRateLimiter(storage) with hiro.Timeline().freeze() as timeline: limit = RateLimitItemPerMinute(10) for i in range(0, 5): self.assertTrue(limiter.hit(limit)) self.assertTrue(limiter.hit(limit)) self.assertEqual( limiter.get_window_stats(limit)[1], 10 - ((i + 1) * 2)) timeline.forward(10) self.assertEqual(limiter.get_window_stats(limit)[1], 0) self.assertFalse(limiter.hit(limit)) timeline.forward(20) self.assertEqual(limiter.get_window_stats(limit)[1], 2) self.assertEqual( limiter.get_window_stats(limit)[0], int(time.time() + 30)) timeline.forward(31) self.assertEqual(limiter.get_window_stats(limit)[1], 10)
def test_moving_window_redis(self): storage = RedisStorage("redis://localhost:6379") limiter = MovingWindowRateLimiter(storage) limit = RateLimitItemPerSecond(10, 2) for i in range(0, 10): self.assertTrue(limiter.hit(limit)) self.assertEqual(limiter.get_window_stats(limit)[1], 10 - (i + 1)) time.sleep(2 * 0.095) self.assertFalse(limiter.hit(limit)) time.sleep(0.4) self.assertTrue(limiter.hit(limit)) self.assertTrue(limiter.hit(limit)) self.assertEqual(limiter.get_window_stats(limit)[1], 0)
def test_test_moving_window(self): with hiro.Timeline().freeze() as timeline: store = MemoryStorage() limit = RateLimitItemPerSecond(2,1) limiter = MovingWindowRateLimiter(store) self.assertTrue(limiter.hit(limit), store) self.assertTrue(limiter.test(limit), store) self.assertTrue(limiter.hit(limit), store) self.assertFalse(limiter.test(limit), store) self.assertFalse(limiter.hit(limit), store)
def test_moving_window_redis(self): storage = RedisStorage("redis://localhost:6379") limiter = MovingWindowRateLimiter(storage) limit = RateLimitItemPerSecond(10, 2) for i in range(0,10): self.assertTrue(limiter.hit(limit)) self.assertEqual(limiter.get_window_stats(limit)[1], 10 - (i + 1)) time.sleep(2*0.095) self.assertFalse(limiter.hit(limit)) time.sleep(0.4) self.assertTrue(limiter.hit(limit)) self.assertTrue(limiter.hit(limit)) self.assertEqual(limiter.get_window_stats(limit)[1], 0)
def test_moving_window_expiry(self): limiter = MovingWindowRateLimiter(self.storage) limit = RateLimitItemPerSecond(2) self.assertTrue(limiter.hit(limit)) time.sleep(0.9) self.assertTrue(limiter.hit(limit)) self.assertFalse(limiter.hit(limit)) time.sleep(0.1) self.assertTrue(limiter.hit(limit)) last = time.time() while time.time() - last <= 1: time.sleep(0.05) self.assertTrue( self.storage.storage.keys("%s/*" % limit.namespace) == [])
def test_pluggable_storage_moving_window(self): class MyStorage(Storage): STORAGE_SCHEME = "mystorage" def incr(self, key, expiry, elastic_expiry=False): return def get(self, key): return 0 def get_expiry(self, key): return time.time() def acquire_entry(self, *a, **k): return True def get_moving_window(self, *a, **k): return (time.time(), 1) storage = storage_from_string("mystorage://") self.assertTrue(isinstance(storage, MyStorage)) MovingWindowRateLimiter(storage)
def test_moving_window_in_memory(self): storage = MemoryStorage() limiter = MovingWindowRateLimiter(storage) with hiro.Timeline().freeze() as timeline: limit = RateLimitItemPerMinute(10) for i in range(0,5): self.assertTrue(limiter.hit(limit)) self.assertTrue(limiter.hit(limit)) self.assertEqual( limiter.get_window_stats(limit)[1], 10 - ((i + 1) * 2) ) timeline.forward(10) self.assertEqual(limiter.get_window_stats(limit)[1], 0) self.assertFalse(limiter.hit(limit)) timeline.forward(20) self.assertEqual(limiter.get_window_stats(limit)[1], 2) self.assertEqual(limiter.get_window_stats(limit)[0], int(time.time() + 30)) timeline.forward(31) self.assertEqual(limiter.get_window_stats(limit)[1], 10)