RazerM / ratelimiter

Simple Python module providing rate limiting
Apache License 2.0
122 stars 29 forks source link

bug in callback triggering possibly related to threading #4

Closed niloch closed 6 years ago

niloch commented 7 years ago

I have a function that uses the requests library that I am decorating with a callback. The callback seems to be triggered more frequently than one would expect. The behavior works as expected when the decorated function returns a string.

import requests
from ratelimiter import RateLimiter

def callback_message(until):
    print('Rate limit reached, sleeping')

@RateLimiter(max_calls=30, period=15, callback=callback_message)
def thing1():
    return requests.get("https://www.google.com")

@RateLimiter(max_calls=30, period=15, callback=callback_message)
def thing2():
    return "thing2"

print("Testing with requests")
for i in range(100):
    if i % 10 == 0:
        print(i)
    thing1()

print("Testing with simple function")
for i in range(100):
    if i % 10 == 0:
        print(i)
    thing2()

This is the output:

Testing with requests
0
10
20
30
Rate limit reached, sleeping
Rate limit reached, sleeping
40
Rate limit reached, sleeping
50
60
Rate limit reached, sleeping
Rate limit reached, sleeping
Rate limit reached, sleeping
Rate limit reached, sleeping
70
Rate limit reached, sleeping
Rate limit reached, sleeping
80
Rate limit reached, sleeping
Rate limit reached, sleeping
90
Rate limit reached, sleeping
Rate limit reached, sleeping
Rate limit reached, sleeping
Rate limit reached, sleeping
Rate limit reached, sleeping
Rate limit reached, sleeping
Rate limit reached, sleeping
Rate limit reached, sleeping

Testing with simple functions
0
10
20
30
Rate limit reached, sleeping
40
50
60
Rate limit reached, sleeping
70
80
90
Rate limit reached, sleeping
RazerM commented 7 years ago

If you print until, you should see that sometimes it's milliseconds between them, because one rate limited call put it beyond the limit.

niloch commented 7 years ago

Hmm. maybe I am confused. It seems the actual rate limiting is working correctly, but when using requests.get the callback is triggered too often. Are you saying that the decorated function is behaving quasi asynchronously?

RazerM commented 7 years ago

one call to thing2 will result in 0 or 1 calls to callback_message

niloch commented 6 years ago

I ended up being able to solve my issue by modifying the __enter__ function to reset the calls queue after sleeping, so the call back was called exactly 3 times in 90 requests.s

 def __enter__(self):
        with self._lock:
            # We want to ensure that no more than max_calls were run in the allowed
            # period. For this, we store the last timestamps of each call and run
            # the rate verification upon each __enter__ call.
            if len(self.calls) >= self.max_calls:
                until = time.time() + self.period - self._timespan
                if self.callback:
                    t = threading.Thread(target=self.callback, args=(until,))
                    t.daemon = True
                    t.start()
                sleeptime = until - time.time()
                if sleeptime > 0:
                    time.sleep(sleeptime)
                    self.calls = collections.deque()
            return self