From 6044f7080484a59b36df34b8048db02cbcb30d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Mon, 30 Jul 2018 17:54:26 +0200 Subject: [PATCH 1/8] Refactor RateLimiter to allow proper subclassing --- lib/tfw/decorators/rate_limiter.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/tfw/decorators/rate_limiter.py b/lib/tfw/decorators/rate_limiter.py index d1972b8..2f21eac 100644 --- a/lib/tfw/decorators/rate_limiter.py +++ b/lib/tfw/decorators/rate_limiter.py @@ -1,25 +1,33 @@ # Copyright (C) 2018 Avatao.com Innovative Learning Kft. # All Rights Reserved. See LICENSE file for details. -from functools import wraps +from functools import wraps, partial from time import time, sleep class RateLimiter: - def __init__(self, rate_per_second): + def __init__(self, rate_per_second, action=sleep): self.min_interval = 1 / float(rate_per_second) + self.action = action + self.fun = None self.last_call = time() def __call__(self, fun): @wraps(fun) def wrapper(*args, **kwargs): - self._limit_rate() - fun(*args, **kwargs) + self.fun = partial(fun, *args, **kwargs) + limit_seconds = self._limit_rate() + if limit_seconds: + self.action(limit_seconds) + return + self.fun() return wrapper def _limit_rate(self): - since_last_call = time() - self.last_call - to_next_call = self.min_interval - since_last_call + seconds_since_last_call = time() - self.last_call + seconds_to_next_call = self.min_interval - seconds_since_last_call self.last_call = time() - if to_next_call > 0: - sleep(to_next_call) + + if seconds_to_next_call > 0: + return seconds_to_next_call + return 0 From eeb36b64881a6bb9fb54582402e789d7e8444268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Mon, 30 Jul 2018 17:55:52 +0200 Subject: [PATCH 2/8] Implement IOLoop based AsyncRateLimiter --- lib/tfw/decorators/rate_limiter.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/tfw/decorators/rate_limiter.py b/lib/tfw/decorators/rate_limiter.py index 2f21eac..6fb020a 100644 --- a/lib/tfw/decorators/rate_limiter.py +++ b/lib/tfw/decorators/rate_limiter.py @@ -31,3 +31,22 @@ class RateLimiter: if seconds_to_next_call > 0: return seconds_to_next_call return 0 + + +class AsyncRateLimiter(RateLimiter): + def __init__(self, rate_per_second, ioloop): + self.ioloop = ioloop + self.last_callback = None + super().__init__( + rate_per_second=rate_per_second, + action=self.async_action + ) + + def async_action(self, seconds_to_next_call): + if self.last_callback: + self.ioloop.remove_timeout(self.last_callback) + + self.last_callback = self.ioloop.call_later( + seconds_to_next_call, + self.fun + ) From ee7adb10be8397809c8a4e1859f8344fc628ec61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Mon, 30 Jul 2018 18:04:24 +0200 Subject: [PATCH 3/8] Refactor AsyncRateLimiter for ease of use (IOLoop passing) --- lib/tfw/decorators/rate_limiter.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/tfw/decorators/rate_limiter.py b/lib/tfw/decorators/rate_limiter.py index 6fb020a..fdfa992 100644 --- a/lib/tfw/decorators/rate_limiter.py +++ b/lib/tfw/decorators/rate_limiter.py @@ -4,6 +4,8 @@ from functools import wraps, partial from time import time, sleep +from tfw.decorators.lazy_property import lazy_property + class RateLimiter: def __init__(self, rate_per_second, action=sleep): @@ -34,19 +36,25 @@ class RateLimiter: class AsyncRateLimiter(RateLimiter): - def __init__(self, rate_per_second, ioloop): - self.ioloop = ioloop - self.last_callback = None + def __init__(self, rate_per_second, ioloop_factory): + self._ioloop_factory = ioloop_factory + self._ioloop = None + self._last_callback = None + super().__init__( rate_per_second=rate_per_second, action=self.async_action ) - def async_action(self, seconds_to_next_call): - if self.last_callback: - self.ioloop.remove_timeout(self.last_callback) + @lazy_property + def ioloop(self): + return self._ioloop_factory() - self.last_callback = self.ioloop.call_later( + def async_action(self, seconds_to_next_call): + if self._last_callback: + self.ioloop.remove_timeout(self._last_callback) + + self._last_callback = self.ioloop.call_later( seconds_to_next_call, self.fun ) From 4679a3494c0d1009085c8d791c433398de12e0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Tue, 31 Jul 2018 05:19:15 +0200 Subject: [PATCH 4/8] Add docstrings to RateLimiter --- lib/tfw/decorators/rate_limiter.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/tfw/decorators/rate_limiter.py b/lib/tfw/decorators/rate_limiter.py index fdfa992..e0c87d1 100644 --- a/lib/tfw/decorators/rate_limiter.py +++ b/lib/tfw/decorators/rate_limiter.py @@ -7,8 +7,32 @@ from time import time, sleep from tfw.decorators.lazy_property import lazy_property +# TODO: add this to sphinx docs class RateLimiter: + """ + Decorator class for rate limiting. + + When applied to a function this decorator will apply rate limiting + if the function is invoked more frequently than rate_per_seconds. + + By default rate limiting means sleeping until the next invocation time + as per __init__ parameter rate_per_seconds. + + Note that this decorator BLOCKS THE THREAD it is being executed on, + so it is only acceptable for stuff running on a separate thread. + + If this is no good for you please refer to AsyncRateLimiter in this module, + which is designed not to block and use the IOLoop it is being called from, + or redefine the action argument of __init__ (which defaults to time.sleep). + """ def __init__(self, rate_per_second, action=sleep): + """ + :param rate_per_second: max frequency the decorated method should be + invoked with + :param action: what to do when rate limiting. defaults to time.sleep, + receives the number of seconds until the next invocation + should occour as an argument + """ self.min_interval = 1 / float(rate_per_second) self.action = action self.fun = None From 128f48702a8da155d4b856aa91d2f473103999c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Tue, 31 Jul 2018 09:14:33 +0200 Subject: [PATCH 5/8] Make AsyncRateLimiter thread safe --- lib/tfw/decorators/rate_limiter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/tfw/decorators/rate_limiter.py b/lib/tfw/decorators/rate_limiter.py index e0c87d1..371e732 100644 --- a/lib/tfw/decorators/rate_limiter.py +++ b/lib/tfw/decorators/rate_limiter.py @@ -59,17 +59,23 @@ class RateLimiter: return 0 +# TODO document this class AsyncRateLimiter(RateLimiter): def __init__(self, rate_per_second, ioloop_factory): self._ioloop_factory = ioloop_factory self._ioloop = None self._last_callback = None + self._make_action_thread_safe() + super().__init__( rate_per_second=rate_per_second, action=self.async_action ) + def _make_action_thread_safe(self): + self.async_action = partial(self.ioloop.add_callback, self.async_action) + @lazy_property def ioloop(self): return self._ioloop_factory() From 3c3012ffe80c0c13f60bb92097e04839196aaf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Tue, 31 Jul 2018 09:19:42 +0200 Subject: [PATCH 6/8] Add docstrings to AsyncRateLimiter --- lib/tfw/decorators/rate_limiter.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/tfw/decorators/rate_limiter.py b/lib/tfw/decorators/rate_limiter.py index 371e732..134dd3e 100644 --- a/lib/tfw/decorators/rate_limiter.py +++ b/lib/tfw/decorators/rate_limiter.py @@ -7,10 +7,9 @@ from time import time, sleep from tfw.decorators.lazy_property import lazy_property -# TODO: add this to sphinx docs class RateLimiter: """ - Decorator class for rate limiting. + Decorator class for rate limiting, blocking. When applied to a function this decorator will apply rate limiting if the function is invoked more frequently than rate_per_seconds. @@ -59,9 +58,22 @@ class RateLimiter: return 0 -# TODO document this class AsyncRateLimiter(RateLimiter): + """ + Decorator class for rate limiting, non-blocking. + + The semantics of the rate limiting are similar to that of RateLimiter, + but this decorator never blocks, instead it adds an async callback version + of the decorated function to the IOLoop to be executed after the rate limiting + has expired. + """ def __init__(self, rate_per_second, ioloop_factory): + """ + :param rate_per_second: max frequency the decorated method should be + invoked with + :param ioloop_factory: callable that should return an instance of the + IOLoop of the application + """ self._ioloop_factory = ioloop_factory self._ioloop = None self._last_callback = None From 8a0928becace6b0bb3a73cebc17f592289a1b8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Tue, 31 Jul 2018 09:55:48 +0200 Subject: [PATCH 7/8] Fix RateLimiter semantics --- lib/tfw/decorators/rate_limiter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tfw/decorators/rate_limiter.py b/lib/tfw/decorators/rate_limiter.py index 134dd3e..0f7f20b 100644 --- a/lib/tfw/decorators/rate_limiter.py +++ b/lib/tfw/decorators/rate_limiter.py @@ -51,10 +51,10 @@ class RateLimiter: def _limit_rate(self): seconds_since_last_call = time() - self.last_call seconds_to_next_call = self.min_interval - seconds_since_last_call - self.last_call = time() if seconds_to_next_call > 0: return seconds_to_next_call + self.last_call = time() return 0 From 3dff144b91e20ff1f6e9ccc2fd7694a894cabe41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Tue, 31 Jul 2018 11:48:41 +0200 Subject: [PATCH 8/8] Fix RateLimiter family debounce stuff --- lib/tfw/decorators/rate_limiter.py | 44 ++++++++++++++---------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/tfw/decorators/rate_limiter.py b/lib/tfw/decorators/rate_limiter.py index 0f7f20b..abe6453 100644 --- a/lib/tfw/decorators/rate_limiter.py +++ b/lib/tfw/decorators/rate_limiter.py @@ -21,31 +21,28 @@ class RateLimiter: so it is only acceptable for stuff running on a separate thread. If this is no good for you please refer to AsyncRateLimiter in this module, - which is designed not to block and use the IOLoop it is being called from, - or redefine the action argument of __init__ (which defaults to time.sleep). + which is designed not to block and use the IOLoop it is being called from. """ - def __init__(self, rate_per_second, action=sleep): + def __init__(self, rate_per_second): """ :param rate_per_second: max frequency the decorated method should be invoked with - :param action: what to do when rate limiting. defaults to time.sleep, - receives the number of seconds until the next invocation - should occour as an argument """ self.min_interval = 1 / float(rate_per_second) - self.action = action self.fun = None self.last_call = time() + def action(self, seconds_to_next_call): + if seconds_to_next_call: + sleep(seconds_to_next_call) + self.fun() + def __call__(self, fun): @wraps(fun) def wrapper(*args, **kwargs): self.fun = partial(fun, *args, **kwargs) limit_seconds = self._limit_rate() - if limit_seconds: - self.action(limit_seconds) - return - self.fun() + self.action(limit_seconds) return wrapper def _limit_rate(self): @@ -62,10 +59,11 @@ class AsyncRateLimiter(RateLimiter): """ Decorator class for rate limiting, non-blocking. - The semantics of the rate limiting are similar to that of RateLimiter, - but this decorator never blocks, instead it adds an async callback version - of the decorated function to the IOLoop to be executed after the rate limiting - has expired. + The semantics of the rate limiting: + - unlike RateLimiter this decorator never blocks, instead it adds an async + callback version of the decorated function to the IOLoop + (to be executed after the rate limiting has expired). + - the timing works similarly to RateLimiter """ def __init__(self, rate_per_second, ioloop_factory): """ @@ -79,24 +77,24 @@ class AsyncRateLimiter(RateLimiter): self._last_callback = None self._make_action_thread_safe() - - super().__init__( - rate_per_second=rate_per_second, - action=self.async_action - ) + super().__init__(rate_per_second=rate_per_second) def _make_action_thread_safe(self): - self.async_action = partial(self.ioloop.add_callback, self.async_action) + self.action = partial(self.ioloop.add_callback, self.action) @lazy_property def ioloop(self): return self._ioloop_factory() - def async_action(self, seconds_to_next_call): + def action(self, seconds_to_next_call): if self._last_callback: self.ioloop.remove_timeout(self._last_callback) self._last_callback = self.ioloop.call_later( seconds_to_next_call, - self.fun + self.fun_with_debounce ) + + def fun_with_debounce(self): + self.last_call = time() + self.fun()