ratelimit 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/ratelimit.rb +110 -0
- metadata +70 -0
data/lib/ratelimit.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'redis-namespace'
|
3
|
+
|
4
|
+
class Ratelimit
|
5
|
+
|
6
|
+
# Create a RateLimit object.
|
7
|
+
#
|
8
|
+
# @param [String] key A name to uniquely identify this rate limit. For example, 'emails'
|
9
|
+
# @param [Redis] redis Redis instance to use. One is created if nothing is passed.
|
10
|
+
# @param [Hash] options Options hash
|
11
|
+
# @option options [Integer] :bucket_span (600) Time span to track in seconds
|
12
|
+
# @option options [Integer] :bucket_interval (5) How many seconds each bucket represents
|
13
|
+
# @option options [Integer] :bucket_expiry (1200) How long we keep data in each bucket before it is auto expired.
|
14
|
+
#
|
15
|
+
# @return [RateLimit] RateLimit instance
|
16
|
+
#
|
17
|
+
def initialize(key, redis = nil, options = {}) #bucket_span = 600, bucket_interval = 5, bucket_expiry = 1200, redis = nil)
|
18
|
+
@key = key
|
19
|
+
@bucket_span = options[:bucket_span] || 600
|
20
|
+
@bucket_interval = options[:bucket_interval] || 5
|
21
|
+
@bucket_expiry = options[:bucket_expiry] || 1200
|
22
|
+
@bucket_count = (@bucket_span / @bucket_interval).round
|
23
|
+
@redis = redis
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add to the counter for a given subject.
|
27
|
+
#
|
28
|
+
# @param [String] subject A unique key to identify the subject. For example, 'user@foo.com'
|
29
|
+
def add(subject)
|
30
|
+
bucket = get_bucket
|
31
|
+
subject = @key + ":" + subject
|
32
|
+
redis.multi do
|
33
|
+
redis.hincrby(subject, bucket, 1)
|
34
|
+
redis.hdel(subject, (bucket + 1) % @bucket_count)
|
35
|
+
redis.hdel(subject, (bucket + 2) % @bucket_count)
|
36
|
+
redis.expire(subject, @bucket_expiry)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the count for a given subject and interval
|
41
|
+
#
|
42
|
+
# @param [String] subject Subject for the count
|
43
|
+
# @param [Integer] interval How far back (in seconds) to retrieve activity.
|
44
|
+
def count(subject, interval)
|
45
|
+
bucket = get_bucket
|
46
|
+
interval = [interval, @bucket_interval].max
|
47
|
+
count = (interval / @bucket_interval).floor
|
48
|
+
subject = @key + ":" + subject
|
49
|
+
counts = redis.multi do
|
50
|
+
redis.hget(subject, bucket)
|
51
|
+
count.downto(1) do
|
52
|
+
bucket -= 1
|
53
|
+
redis.hget(subject, (bucket + @bucket_count) % @bucket_count)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
return counts.inject(0) {|a, i| a += i.to_i}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Check if the rate limit has been exceeded.
|
60
|
+
#
|
61
|
+
# @param [String] subject Subject to check
|
62
|
+
# @param [Hash] options Options hash
|
63
|
+
# @option options [Integer] :interval How far back to retrieve activity.
|
64
|
+
# @option options [Integer] :threshold Maximum number of actions
|
65
|
+
def exceeded?(subject, options = {})
|
66
|
+
return count(subject, options[:interval]) >= options[:threshold]
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check if the rate limit is within bounds
|
70
|
+
#
|
71
|
+
# @param [String] subject Subject to check
|
72
|
+
# @param [Hash] options Options hash
|
73
|
+
# @option options [Integer] :interval How far back to retrieve activity.
|
74
|
+
# @option options [Integer] :threshold Maximum number of actions
|
75
|
+
def within_bounds?(subject, options = {})
|
76
|
+
return !exceeded?(subject, options)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Execute a block once the rate limit is within bounds
|
80
|
+
# *WARNING* This will block the current thread until the rate limit is within bounds.
|
81
|
+
#
|
82
|
+
# @param [String] subject Subject for this rate limit
|
83
|
+
# @param [Hash] options Options hash
|
84
|
+
# @option options [Integer] :interval How far back to retrieve activity.
|
85
|
+
# @option options [Integer] :threshold Maximum number of actions
|
86
|
+
# @yield The block to be run
|
87
|
+
#
|
88
|
+
# @example Send an email as long as we haven't send 5 in the last 10 minutes
|
89
|
+
# ratelimit.exec_with_threshold(email, [:threshold => 5, :interval => 600]) do
|
90
|
+
# send_another_email
|
91
|
+
# end
|
92
|
+
def exec_within_threshold(subject, options = {}, &block)
|
93
|
+
options[:threshold] ||= 30
|
94
|
+
options[:interval] ||= 30
|
95
|
+
while exceeded?(subject, options)
|
96
|
+
sleep @bucket_interval
|
97
|
+
end
|
98
|
+
yield(self)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def get_bucket(time = Time.now.to_i)
|
104
|
+
((time % @bucket_span) / @bucket_interval).floor
|
105
|
+
end
|
106
|
+
|
107
|
+
def redis
|
108
|
+
@redis ||= Redis::Namespace.new(:ratelimit, :redis => @redis || Redis.new)
|
109
|
+
end
|
110
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ratelimit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- E.J. Finneran
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-29 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: &70236973620740 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70236973620740
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: redis-namespace
|
27
|
+
requirement: &70236973620200 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.0.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70236973620200
|
36
|
+
description: This library uses Redis to track the number of actions for a given subject
|
37
|
+
over a flexible time frame.
|
38
|
+
email:
|
39
|
+
- ej.finneran@gmail.com
|
40
|
+
executables: []
|
41
|
+
extensions: []
|
42
|
+
extra_rdoc_files: []
|
43
|
+
files:
|
44
|
+
- lib/ratelimit.rb
|
45
|
+
homepage: http://github.com/ejfinneran/ratelimit
|
46
|
+
licenses: []
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.8.11
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Rate limiting backed by redis
|
69
|
+
test_files: []
|
70
|
+
has_rdoc:
|