forked from ejfinneran/ratelimit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathratelimit.rb
119 lines (109 loc) · 4.21 KB
/
ratelimit.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
require 'redis'
require 'redis-namespace'
class Ratelimit
# Create a RateLimit object.
#
# @param [String] key A name to uniquely identify this rate limit. For example, 'emails'
# @param [Hash] options Options hash
# @option options [Integer] :bucket_span (600) Time span to track in seconds
# @option options [Integer] :bucket_interval (5) How many seconds each bucket represents
# @option options [Integer] :bucket_expiry (@bucket_span) How long we keep data in each bucket before it is auto expired. Cannot be larger than the bucket_span.
# @option options [Redis] :redis (nil) Redis client if you need to customize connection options
#
# @return [RateLimit] RateLimit instance
#
def initialize(key, options = {})
@key = key
unless options.is_a?(Hash)
raise ArgumentError.new("Redis object is now passed in via the options hash - options[:redis]")
end
@bucket_span = options[:bucket_span] || 600
@bucket_interval = options[:bucket_interval] || 5
@bucket_expiry = options[:bucket_expiry] || @bucket_span
if @bucket_expiry > @bucket_span
raise ArgumentError.new("Bucket expiry cannot be larger than the bucket span")
end
@bucket_count = (@bucket_span / @bucket_interval).round
@redis = options[:redis]
end
# Add to the counter for a given subject.
#
# @param [String] subject A unique key to identify the subject. For example, '[email protected]'
# @param [Integer] count The number by which to increase the counter
#
# @return [Integer] The counter value
def add(subject, count = 1)
bucket = get_bucket
subject = "#{@key}:#{subject}"
redis.multi do
redis.hincrby(subject, bucket, count)
redis.hdel(subject, (bucket + 1) % @bucket_count)
redis.hdel(subject, (bucket + 2) % @bucket_count)
redis.expire(subject, @bucket_expiry)
end.first
end
# Returns the count for a given subject and interval
#
# @param [String] subject Subject for the count
# @param [Integer] interval How far back (in seconds) to retrieve activity.
def count(subject, interval)
bucket = get_bucket
interval = [interval, @bucket_interval].max
count = (interval / @bucket_interval).floor
subject = "#{@key}:#{subject}"
counts = redis.multi do
redis.hget(subject, bucket)
count.downto(1) do
bucket -= 1
redis.hget(subject, (bucket + @bucket_count) % @bucket_count)
end
end
return counts.inject(0) {|a, i| a += i.to_i}
end
# Check if the rate limit has been exceeded.
#
# @param [String] subject Subject to check
# @param [Hash] options Options hash
# @option options [Integer] :interval How far back to retrieve activity.
# @option options [Integer] :threshold Maximum number of actions
def exceeded?(subject, options = {})
return count(subject, options[:interval]) >= options[:threshold]
end
# Check if the rate limit is within bounds
#
# @param [String] subject Subject to check
# @param [Hash] options Options hash
# @option options [Integer] :interval How far back to retrieve activity.
# @option options [Integer] :threshold Maximum number of actions
def within_bounds?(subject, options = {})
return !exceeded?(subject, options)
end
# Execute a block once the rate limit is within bounds
# *WARNING* This will block the current thread until the rate limit is within bounds.
#
# @param [String] subject Subject for this rate limit
# @param [Hash] options Options hash
# @option options [Integer] :interval How far back to retrieve activity.
# @option options [Integer] :threshold Maximum number of actions
# @yield The block to be run
#
# @example Send an email as long as we haven't send 5 in the last 10 minutes
# ratelimit.exec_with_threshold(email, [:threshold => 5, :interval => 600]) do
# send_another_email
# end
def exec_within_threshold(subject, options = {}, &block)
options[:threshold] ||= 30
options[:interval] ||= 30
while exceeded?(subject, options)
sleep @bucket_interval
end
yield(self)
end
private
def get_bucket(time = Time.now.to_i)
((time % @bucket_span) / @bucket_interval).floor
end
def redis
@redis ||= Redis::Namespace.new(:ratelimit, :redis => @redis || Redis.new)
end
end