Skip to content

Instantly share code, notes, and snippets.

@rilian
Last active September 17, 2015 15:34
Show Gist options
  • Save rilian/54b3d6854ef0d8d0ba51 to your computer and use it in GitHub Desktop.
Save rilian/54b3d6854ef0d8d0ba51 to your computer and use it in GitHub Desktop.
rate limit
rescue_from RateLimit::ExceededError do |e|
render(json: errors_json(e.message), status: 429)
end
# ...
gem 'redis'
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
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
# 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