Last active
September 17, 2015 15:34
-
-
Save rilian/54b3d6854ef0d8d0ba51 to your computer and use it in GitHub Desktop.
rate limit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
rescue_from RateLimit::ExceededError do |e| | |
render(json: errors_json(e.message), status: 429) | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ... | |
gem 'redis' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'redis' | |
module RateLimit | |
class ExceededError < StandardError | |
def initialize(wait_time) | |
@wait_time = wait_time | |
end | |
def message | |
"Please wait for #{@wait_time} seconds before repeating the request" | |
end | |
end | |
def self.shape!(action:, limit:, interval: 3600) | |
raise ArgumentError.new('action cannot be blank') if action.blank? | |
raise ArgumentError.new('limit must be greater than 0') if limit <= 0 | |
keys = Redis.current.keys("rate_limit:#{action}:*") | |
time_sec = Time.zone.now.to_i | |
if keys.size >= limit | |
wait = interval - (time_sec - Redis.current.get(keys.last).to_i).abs | |
raise RateLimit::ExceededError.new(wait.to_s) | |
else | |
key = "rate_limit:#{action}:#{time_sec}.#{Time.zone.now.usec}" | |
Redis.current.set(key, time_sec, ex: interval) | |
end | |
end | |
def self.cleanup(action) | |
raise ArgumentError.new('action cannot be blank') if action.blank? | |
keys = Redis.current.keys("rate_limit:#{action}:*") | |
Redis.current.del(keys) unless keys.empty? | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
describe RateLimit do | |
describe 'public methods' do | |
before { Redis.current.flushall } | |
describe 'shape!' do | |
context 'validates params' do | |
describe 'action' do | |
it 'valid when action present' do | |
expect { RateLimit.shape!(action: 'verb', limit: 1) }.not_to raise_error | |
end | |
it 'invalid when action blank' do | |
expect { RateLimit.shape!(action: '', limit: 1) }.to raise_error(ArgumentError, 'action cannot be blank') | |
end | |
end | |
describe 'limit' do | |
it 'valid when limit present' do | |
expect { RateLimit.shape!(action: 'verb', limit: 1) }.not_to raise_error | |
end | |
it 'invalid when limit not greater than 0' do | |
expect { RateLimit.shape!(action: 'verb', limit: 0) } | |
.to raise_error(ArgumentError, 'limit must be greater than 0') | |
end | |
end | |
end | |
context 'when rate not exceeded' do | |
before { Timecop.freeze('2012-11-22T08:00:00-0500') } | |
it 'saves action to log' do | |
expect(Redis.current.keys('rate_limit:*').size).to eq 0 | |
expect { RateLimit.shape!(action: 'make_pies', limit: 5) }.not_to raise_error | |
expect(Redis.current.keys('*')).to eq ['rate_limit:make_pies:1353589200.0'] | |
end | |
end | |
context 'when rate exceeded' do | |
before { Timecop.freeze('2012-11-22T08:00:00-0500') } | |
it 'raises exception' do | |
RateLimit.shape!(action: 'make_pies', limit: 1) | |
Timecop.freeze('2012-11-22T08:10:00-0500') | |
expect { RateLimit.shape!(action: 'make_pies', limit: 1) } | |
.to raise_error(RateLimit::ExceededError) do |e| | |
expect(e.message).to eq 'Please wait for 3000 seconds before repeating the request' | |
end | |
end | |
end | |
end | |
describe 'cleanup' do | |
before do | |
3.times { RateLimit.shape!(action: 'make_pies', limit: 10) } | |
2.times { RateLimit.shape!(action: 'make_cookies', limit: 10) } | |
end | |
it 'removes previous records' do | |
expect(Redis.current.keys.size).to eq 5 | |
expect(Redis.current.keys('rate_limit:make_pies:*').size).to eq 3 | |
expect(Redis.current.keys('rate_limit:make_cookies:*').size).to eq 2 | |
RateLimit.cleanup('make_pies') | |
expect(Redis.current.keys.size).to eq 2 | |
expect(Redis.current.keys('rate_limit:make_pies:*').size).to eq 0 | |
expect(Redis.current.keys('rate_limit:make_cookies:*').size).to eq 2 | |
end | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# in some controller action | |
def some_action | |
# allow few invalid passwords to be entered, until doorkeeper rescue will be solved | |
RateLimit.shape!(action: "3rdparty:buzzsumo", limit: ENV['BUZZSUMO_REQUESTS_PER_SECOND'].to_i) | |
# ... | |
if success? | |
RateLimit.cleanup("3rdparty:buzzsumo") | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment