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.
Files changed (2) hide show
  1. data/lib/ratelimit.rb +110 -0
  2. 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: