Skip to content

Commit

Permalink
Sliding window rate limiting
Browse files Browse the repository at this point in the history
Switched the algorithm to use a circular buffer
based on a redis list
  • Loading branch information
mattvanhorn committed May 25, 2013
1 parent e72694c commit d5958f8
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 11 deletions.
30 changes: 20 additions & 10 deletions lib/rate_limiter.rb
Expand Up @@ -27,26 +27,36 @@ def can_perform?
def performed!
return if rate_unlimited?

result = $redis.incr(@key).to_i
$redis.expire(@key, @secs) if result == 1
if result > @max

# In case we go over, clamp it to the maximum
$redis.decr(@key)

raise LimitExceeded.new($redis.ttl(@key))
if is_under_limit?
# simple ring buffer.
$redis.lpush(@key, Time.now.to_i)
$redis.ltrim(@key, 0, @max - 1)
else
raise LimitExceeded.new(seconds_to_wait)
end
end

def rollback!
return if RateLimiter.disabled?
$redis.decr(@key)
$redis.lpop(@key)
end

private

def seconds_to_wait
@secs - age_of_oldest
end

def age_of_oldest
# age of oldest event in buffer, in seconds
Time.now.to_i - $redis.lrange(@key, -1, -1).first.to_i
end

def is_under_limit?
$redis.get(@key).to_i < @max
# number of events in buffer less than max allowed? OR
($redis.llen(@key) < @max) ||
# age bigger than silding window size?
(age_of_oldest > @secs)
end

def rate_unlimited?
Expand Down
2 changes: 1 addition & 1 deletion spec/components/rate_limiter_spec.rb
Expand Up @@ -50,7 +50,7 @@
end

it "raises an error the third time called" do
lambda { rate_limiter.performed! }.should raise_error
lambda { rate_limiter.performed! }.should raise_error(RateLimiter::LimitExceeded)
end

context "as an admin/moderator" do
Expand Down

0 comments on commit d5958f8

Please sign in to comment.