def __init__(self, *args, **kwargs): super(HyacinthBot, self).__init__(*args, **kwargs) self.password = config.password self.karma_store = KarmaStore(config.karma_db_path) self.eightball = EightBall(config.eightball_answers_path) self.markov = Markov(config.markov_db_path) self.roller = Roller() self.rate_limiter = RateLimiter() self.greeter = Greeter(config.greetings_path, config.greeting_probability)
def test_user_1_rps(): class TestTimer(MockTimer): def get_times(self) -> []: return [0, 500, 1000, 1001, 1500, 2000, 2001] rate_limiter = RateLimiter(TestTimer(), bucket_ms_interval=1) rate_limiter.configure_limit(user_id=1, rps=1) assert rate_limiter.process_request(1) # for 0 stamp assert not rate_limiter.process_request(1) # 500 assert rate_limiter.process_request(1) # 1000 this will pass because exactly 1 sec passed since last request assert not rate_limiter.process_request(1) # 1001 assert not rate_limiter.process_request(1) # 1500 assert rate_limiter.process_request(1) # 2000 this will pass because exactly 1 sec passed since last request assert not rate_limiter.process_request(1) # 2001
def test_global_2_rps(): class TestTimer(MockTimer): def get_times(self) -> []: return [0, 500, 1000, 1001, 1500, 2000, 2001] rate_limiter = RateLimiter(TestTimer(), bucket_ms_interval=1) rate_limiter.configure_global_limit(rps=2) assert rate_limiter.process_request(1) # for 0 stamp assert rate_limiter.process_request(2) # 500 assert rate_limiter.process_request(3) # 1000 this will pass because exactly 1 sec passed since request at 0 # and only one request in between assert not rate_limiter.process_request(4) # 1001 assert rate_limiter.process_request(5) # 1500 this will pass because exactly 1 sec passed since request at 500 # and only one request in between assert rate_limiter.process_request(6) # 2000 assert not rate_limiter.process_request(7) # 2001
def test_two_users_1_rps(): class TestTimer(MockTimer): def get_times(self) -> []: return [0, 500, 1000, 1001, 1500, 2000, 2001] rate_limiter = RateLimiter(TestTimer(), bucket_ms_interval=1) rate_limiter.configure_limit(user_id=1, rps=1) rate_limiter.configure_limit(user_id=2, rps=1) # because users have their own limits configured, they do not overlap assert rate_limiter.process_request(1) # for 0 stamp and user 1 assert rate_limiter.process_request(2) # 500 it's OK, because it is user 2 assert rate_limiter.process_request(1) # 1000 will pass because exactly 1 sec passed since last request for user 1 assert not rate_limiter.process_request(1) # 1001 user 1 just made a request, won't pass assert rate_limiter.process_request(2) # 1500 will pass because exactly 1 sec passed since last request for user 2 assert not rate_limiter.process_request(2) # 2000 user 2 made a request 500ms ago assert rate_limiter.process_request(1) # 2001 will pass because more than 1 sec passed since a request for user 1
def test_user_1_rps_and_unlimited_for_others(): class TestTimer(MockTimer): def get_times(self) -> []: return [0, 500, 550, 700, 800, 900, 999, 1001] rate_limiter = RateLimiter(TestTimer(), bucket_ms_interval=1) rate_limiter.configure_limit(user_id=1, rps=1) # user 1 has only limits configured, but others are unlimited assert rate_limiter.process_request(1) # for 0 stamp and user 1 assert rate_limiter.process_request(2) # 500 other users unlimited assert rate_limiter.process_request(3) # 550 assert rate_limiter.process_request(4) # 700 assert rate_limiter.process_request(5) # 800 assert rate_limiter.process_request(6) # 900 assert not rate_limiter.process_request(1) # 999 but user 1 already exceeded a limit assert rate_limiter.process_request(1) # 1001 user 1 may execute, because 1 sec already passed since last success
class HyacinthBot(BaseIRCBot): """An IRC bot.""" nickname = config.nickname def __init__(self, *args, **kwargs): super(HyacinthBot, self).__init__(*args, **kwargs) self.password = config.password self.karma_store = KarmaStore(config.karma_db_path) self.eightball = EightBall(config.eightball_answers_path) self.markov = Markov(config.markov_db_path) self.roller = Roller() self.rate_limiter = RateLimiter() self.greeter = Greeter(config.greetings_path, config.greeting_probability) # callbacks for events def privmsg(self, user, channel, msg): """This will get called when the bot receives a message.""" user = user.split('!', 1)[0] self.logger.log(msg) # Check to see if they're sending me a private message if channel == self.nickname: msg = "It isn't nice to whisper! Play nice with the group." self.msg(user, msg) return # Otherwise check to see if it is a message directed at me if msg.startswith(self.nickname + ":"): msg = "%s: I am the little death that brings total oblivion" % user self.msg(channel, msg) self.logger.log("<%s> %s" % (self.nickname, msg)) return self.process_message(user, channel, msg) def userJoined(self, user, channel): """Greets a user when they join the channel""" greeting = self.greeter.greet(user) if greeting: self.msg(channel, greeting) def process_message(self, user, channel, msg): self.record_karmas(user, channel, msg) self.markov.add_single_line(msg) if msg.startswith('!'): self.rate_limiter.add_request(user) if not self.rate_limiter.is_rate_limited(user): self.process_command(user, channel, msg) else: self.send_markov_sentence(user, channel, msg) def process_command(self, user, channel, msg): """Decide how to act upon a message, given that it is a command i.e. begins with !, e.g. "!karma" """ if msg.startswith('!karma'): self.process_karmastring(user, channel, msg) elif msg.startswith('!8ball') and msg.endswith('?'): self.msg(channel, self.eightball.get_answer()) elif msg.startswith('!markov'): new_msg = msg.replace('!markov', '') self.send_markov_sentence(user, channel, new_msg, force=True) elif msg.startswith('!roll'): new_msg = msg.replace('!roll', '') self.roll(user, channel, new_msg) elif msg.startswith('!commands'): self.msg(channel, '!karma, !8ball, !markov, !roll, !commands') def send_markov_sentence(self, user, channel, message, force=False): thing_to_say = self.markov.generate_sentence(message) if force or (len(thing_to_say.split()) > 5 and random.random() > 0.995): self.msg(channel, thing_to_say) def process_karmastring(self, user, channel, msg): # this is a stupid hack, since if there's not a space at the end # it fails strangely msg = msg + ' ' split_msg = msg.split(' ') if len(split_msg) == 1: requested_user = user else: requested_user = split_msg[1] msg = self.karma_store.get_karma(requested_user) self.msg(channel, str(msg)) def record_karmas(self, user, channel, msg): karmas = karma_regex.findall(msg) if not karmas: return for karma in karmas: recipient = karma[:-2] # on account of the ++ or -- if recipient == user: self.msg(channel, 'no altering your own karma, %s' % user) continue if karma.endswith('--'): self.karma_store.record_karma(recipient, up=False) else: self.karma_store.record_karma(recipient, up=True) def roll(self, user, channel, msg): die, value = self.roller.roll(msg) roll_string = '%s rolls %s... %d' % (user, die, value) self.msg(channel, roll_string)
def test_10_ms_bucket_interval(): """ This test expresses a drawback of designed rate limiter. Even the time interval between two requests with timestamps 1007, 2001 is shorter than 1 second it still passes. The reason is we split our tracker by 10 milliseconds interval, so once we performed a bucketing the distance between two buckets would be exactly 1 second which is valid. The case won't pass for 1ms buckets. So, with 10ms wide bucket we produce 100 buckets, we may incorrectly identify a distance for half timestamps within a bucket, that gives us 0.5% error. With 1ms the error is 0.05%. We may decrease the error by decreasing the width of the bucket, but this will impact space and time complexity. Overall the number of buckets will not exceed 1000/[bucket width], so we can talk about effective constant time and space complexity. :return: """ class TestTimer(MockTimer): def get_times(self) -> []: return [1007, 2001] rate_limiter = RateLimiter(TestTimer(), bucket_ms_interval=10) rate_limiter.configure_limit(user_id=1, rps=1) assert rate_limiter.process_request(1) # for 1007 stamp assert rate_limiter.process_request(1) # 2001 rate_limiter = RateLimiter(TestTimer(), bucket_ms_interval=1) rate_limiter.configure_limit(user_id=1, rps=1) assert rate_limiter.process_request(1) # for 1007 stamp assert not rate_limiter.process_request(1) # 2001
def test_user_contributes_to_global_limit(): class TestTimer(MockTimer): def get_times(self) -> []: return [0, 500, 1000, 1001, 1500, 2000, 2001] rate_limiter = RateLimiter(TestTimer(), bucket_ms_interval=1) rate_limiter.configure_limit(user_id=1, rps=1) rate_limiter.configure_global_limit(rps=1) # user 1 makes 1 request for in 1 second interval and contribute to global limit, second user can't execute during # the same interval assert rate_limiter.process_request(1) # for 0 stamp user 1 assert not rate_limiter.process_request(2) # 500 since user 1 already made a request assert rate_limiter.process_request(1) # 1000 this will pass because exactly 1 sec passed since last request assert not rate_limiter.process_request(2) # 1001 since user 1 just made a request assert not rate_limiter.process_request(1) # 1500 assert rate_limiter.process_request(1) # 2000 this will pass because exactly 1 sec passed since last request assert not rate_limiter.process_request(2) # 2001