From d5958f877984383b30f6340aeeb1734caa1711bd Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 25 May 2013 12:37:28 -0700 Subject: [PATCH] Sliding window rate limiting Switched the algorithm to use a circular buffer based on a redis list --- lib/rate_limiter.rb | 30 ++++++++++++++++++---------- spec/components/rate_limiter_spec.rb | 2 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/lib/rate_limiter.rb b/lib/rate_limiter.rb index 5d794e9c9..4c349e4f7 100644 --- a/lib/rate_limiter.rb +++ b/lib/rate_limiter.rb @@ -27,26 +27,36 @@ class RateLimiter 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? diff --git a/spec/components/rate_limiter_spec.rb b/spec/components/rate_limiter_spec.rb index 8d763a400..7b2f27f50 100644 --- a/spec/components/rate_limiter_spec.rb +++ b/spec/components/rate_limiter_spec.rb @@ -50,7 +50,7 @@ describe RateLimiter do 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