redis-throttler 0.1.4 → 0.1.5
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.
- checksums.yaml +4 -4
- data/lib/redis-throttler/base.rb +120 -0
- data/lib/redis-throttler/model.rb +2 -4
- data/lib/redis-throttler/version.rb +2 -2
- data/lib/redis-throttler.rb +5 -116
- data/spec/spec_helper.rb +1 -0
- data/spec/throttler_spec.rb +4 -4
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3648bd744161a9c30693fd53fb7bfb490ab54f3
|
4
|
+
data.tar.gz: 1b7c3424b04912da66ed591db45e14a36e518e5d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7bff0a3c2299681c052da3d0333f6c7858ae5b689a2c434ead0e18de1bcfa83dbc94b9f241afb4509fe05c5eef3c2d5b9c5cd18cc0faf6b2aae0b20abd8f0bfa
|
7
|
+
data.tar.gz: bf0eac98c7f1cff826f20c79330f135b4252d58d8b47b77ce45000e20521d0df3dffebd52450efd4abb1d330e0217c2c289d15600d426a8f27610cf3af6ee1cd
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module RedisThrottler
|
2
|
+
class Base
|
3
|
+
# Create a RedisThrottler object.
|
4
|
+
#
|
5
|
+
# @param [String] key A name to uniquely identify this rate limit. For example, 'emails'
|
6
|
+
# @param [Hash] options Options hash
|
7
|
+
# @option options [Integer] :bucket_span (600) Time span to track in seconds
|
8
|
+
# @option options [Integer] :bucket_interval (5) How many seconds each bucket represents
|
9
|
+
# @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.
|
10
|
+
# @option options [Redis] :redis (nil) Redis client if you need to customize connection options
|
11
|
+
#
|
12
|
+
# @return [RedisThrottler] RedisThrottler instance
|
13
|
+
#
|
14
|
+
def initialize(key, options = {})
|
15
|
+
@key = key
|
16
|
+
@bucket_span = options[:bucket_span] || 600
|
17
|
+
@bucket_interval = options[:bucket_interval] || 5
|
18
|
+
@bucket_expiry = options[:bucket_expiry] || @bucket_span
|
19
|
+
if @bucket_expiry > @bucket_span
|
20
|
+
raise ArgumentError.new("Bucket expiry cannot be larger than the bucket span")
|
21
|
+
end
|
22
|
+
@bucket_count = (@bucket_span / @bucket_interval).round
|
23
|
+
if @bucket_count < 3
|
24
|
+
raise ArgumentError.new("Cannot have less than 3 buckets")
|
25
|
+
end
|
26
|
+
@redis = options[:redis]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Increment counter for a given subject.
|
30
|
+
#
|
31
|
+
# @param [String] subject A unique key to identify the subject. For example, 'user@foo.com'
|
32
|
+
# @param [Integer] count The number by which to increment the counter
|
33
|
+
#
|
34
|
+
# @return [Integer] increments within interval
|
35
|
+
def add(subject, count = 1)
|
36
|
+
bucket = get_bucket
|
37
|
+
subject = "#{@key}:#{subject}"
|
38
|
+
redis.pipelined do
|
39
|
+
redis.hincrby(subject, bucket, count)
|
40
|
+
redis.hdel(subject, (bucket + 1) % @bucket_count)
|
41
|
+
redis.hdel(subject, (bucket + 2) % @bucket_count)
|
42
|
+
redis.expire(subject, @bucket_expiry)
|
43
|
+
end.first
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the count for a given subject and interval
|
47
|
+
#
|
48
|
+
# @param [String] subject Subject for the count
|
49
|
+
# @param [Integer] interval How far back (in seconds) to retrieve activity.
|
50
|
+
#
|
51
|
+
# @return [Integer] current count for subject
|
52
|
+
def count(subject, interval)
|
53
|
+
bucket = get_bucket
|
54
|
+
interval = [interval, @bucket_interval].max
|
55
|
+
count = (interval / @bucket_interval).floor
|
56
|
+
subject = "#{@key}:#{subject}"
|
57
|
+
|
58
|
+
keys = (0..count - 1).map do |i|
|
59
|
+
(bucket - i) % @bucket_count
|
60
|
+
end
|
61
|
+
redis.hmget(subject, *keys).inject(0) {|a, i| a + i.to_i}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Check if the rate limit has been exceeded.
|
65
|
+
#
|
66
|
+
# @param [String] subject Subject to check
|
67
|
+
# @param [Hash] options Options hash
|
68
|
+
# @option options [Integer] :interval How far back to retrieve activity.
|
69
|
+
# @option options [Integer] :threshold Maximum number of actions
|
70
|
+
#
|
71
|
+
# @return [Boolean] true if exceeded
|
72
|
+
def exceeded?(subject, options = {})
|
73
|
+
count(subject, options[:interval]) >= options[:threshold]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Check if the rate limit is within bounds
|
77
|
+
#
|
78
|
+
# @param [String] subject Subject to check
|
79
|
+
# @param [Hash] options Options hash
|
80
|
+
# @option options [Integer] :interval How far back to retrieve activity.
|
81
|
+
# @option options [Integer] :threshold Maximum number of actions
|
82
|
+
#
|
83
|
+
# @return [Integer] true if within bounds
|
84
|
+
def within_bounds?(subject, options = {})
|
85
|
+
!exceeded?(subject, options)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Execute a block once the rate limit is within bounds
|
89
|
+
# *WARNING* This will block the current thread until the rate limit is within bounds.
|
90
|
+
#
|
91
|
+
# @param [String] subject Subject for this rate limit
|
92
|
+
# @param [Hash] options Options hash
|
93
|
+
# @option options [Integer] :interval How far back to retrieve activity.
|
94
|
+
# @option options [Integer] :threshold Maximum number of actions
|
95
|
+
# @yield The block to be run
|
96
|
+
#
|
97
|
+
# @example Send an email as long as we haven't send 5 in the last 10 minutes
|
98
|
+
# RedisThrottler.exec_with_threshold(email, [:threshold => 5, :interval => 600]) do
|
99
|
+
# send_another_email
|
100
|
+
# end
|
101
|
+
def exec_within_threshold(subject, options = {}, &block)
|
102
|
+
options[:threshold] ||= 30
|
103
|
+
options[:interval] ||= 30
|
104
|
+
while exceeded?(subject, options)
|
105
|
+
sleep @bucket_interval
|
106
|
+
end
|
107
|
+
yield(self)
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def get_bucket(time = Time.now.to_i)
|
113
|
+
((time % @bucket_span) / @bucket_interval).floor
|
114
|
+
end
|
115
|
+
|
116
|
+
def redis
|
117
|
+
@redis ||= Redis.new(host: '192.168.99.100', port: 32771)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -1,6 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
class RedisThrottler
|
1
|
+
module RedisThrottler
|
4
2
|
module Model
|
5
3
|
|
6
4
|
def self.included(base)
|
@@ -20,7 +18,7 @@ class RedisThrottler
|
|
20
18
|
threshold = opts[:for] || 900
|
21
19
|
interval = opts[:interval] || 5
|
22
20
|
|
23
|
-
limiter = RedisThrottler.new("#{klass}:#{key}", bucket_interval: interval, bucket_span: threshold)
|
21
|
+
limiter = RedisThrottler::Base.new("#{klass}:#{key}", bucket_interval: interval, bucket_span: threshold)
|
24
22
|
@limits[key] = "#{subject.to_s} limit #{limit} per #{threshold} sec"
|
25
23
|
|
26
24
|
# includes('?') will return true
|
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = '0.1.
|
1
|
+
module RedisThrottler
|
2
|
+
VERSION = '0.1.5'
|
3
3
|
end
|
data/lib/redis-throttler.rb
CHANGED
@@ -1,120 +1,9 @@
|
|
1
1
|
require 'redis'
|
2
|
+
require 'redis-throttler/model'
|
3
|
+
require 'redis-throttler/base'
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
# @param [String] key A name to uniquely identify this rate limit. For example, 'emails'
|
7
|
-
# @param [Hash] options Options hash
|
8
|
-
# @option options [Integer] :bucket_span (600) Time span to track in seconds
|
9
|
-
# @option options [Integer] :bucket_interval (5) How many seconds each bucket represents
|
10
|
-
# @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.
|
11
|
-
# @option options [Redis] :redis (nil) Redis client if you need to customize connection options
|
12
|
-
#
|
13
|
-
# @return [RedisThrottler] RedisThrottler instance
|
14
|
-
#
|
15
|
-
def initialize(key, options = {})
|
16
|
-
@key = key
|
17
|
-
@bucket_span = options[:bucket_span] || 600
|
18
|
-
@bucket_interval = options[:bucket_interval] || 5
|
19
|
-
@bucket_expiry = options[:bucket_expiry] || @bucket_span
|
20
|
-
if @bucket_expiry > @bucket_span
|
21
|
-
raise ArgumentError.new("Bucket expiry cannot be larger than the bucket span")
|
22
|
-
end
|
23
|
-
@bucket_count = (@bucket_span / @bucket_interval).round
|
24
|
-
if @bucket_count < 3
|
25
|
-
raise ArgumentError.new("Cannot have less than 3 buckets")
|
26
|
-
end
|
27
|
-
@redis = options[:redis]
|
28
|
-
end
|
29
|
-
|
30
|
-
# Increment counter for a given subject.
|
31
|
-
#
|
32
|
-
# @param [String] subject A unique key to identify the subject. For example, 'user@foo.com'
|
33
|
-
# @param [Integer] count The number by which to increment the counter
|
34
|
-
#
|
35
|
-
# @return [Integer] increments within interval
|
36
|
-
def add(subject, count = 1)
|
37
|
-
bucket = get_bucket
|
38
|
-
subject = "#{@key}:#{subject}"
|
39
|
-
redis.pipelined do
|
40
|
-
redis.hincrby(subject, bucket, count)
|
41
|
-
redis.hdel(subject, (bucket + 1) % @bucket_count)
|
42
|
-
redis.hdel(subject, (bucket + 2) % @bucket_count)
|
43
|
-
redis.expire(subject, @bucket_expiry)
|
44
|
-
end.first
|
45
|
-
end
|
46
|
-
|
47
|
-
# Returns the count for a given subject and interval
|
48
|
-
#
|
49
|
-
# @param [String] subject Subject for the count
|
50
|
-
# @param [Integer] interval How far back (in seconds) to retrieve activity.
|
51
|
-
#
|
52
|
-
# @return [Integer] current count for subject
|
53
|
-
def count(subject, interval)
|
54
|
-
bucket = get_bucket
|
55
|
-
interval = [interval, @bucket_interval].max
|
56
|
-
count = (interval / @bucket_interval).floor
|
57
|
-
subject = "#{@key}:#{subject}"
|
58
|
-
|
59
|
-
keys = (0..count - 1).map do |i|
|
60
|
-
(bucket - i) % @bucket_count
|
61
|
-
end
|
62
|
-
redis.hmget(subject, *keys).inject(0) {|a, i| a + i.to_i}
|
63
|
-
end
|
64
|
-
|
65
|
-
# Check if the rate limit has been exceeded.
|
66
|
-
#
|
67
|
-
# @param [String] subject Subject to check
|
68
|
-
# @param [Hash] options Options hash
|
69
|
-
# @option options [Integer] :interval How far back to retrieve activity.
|
70
|
-
# @option options [Integer] :threshold Maximum number of actions
|
71
|
-
#
|
72
|
-
# @return [Boolean] true if exceeded
|
73
|
-
def exceeded?(subject, options = {})
|
74
|
-
count(subject, options[:interval]) >= options[:threshold]
|
75
|
-
end
|
76
|
-
|
77
|
-
# Check if the rate limit is within bounds
|
78
|
-
#
|
79
|
-
# @param [String] subject Subject to check
|
80
|
-
# @param [Hash] options Options hash
|
81
|
-
# @option options [Integer] :interval How far back to retrieve activity.
|
82
|
-
# @option options [Integer] :threshold Maximum number of actions
|
83
|
-
#
|
84
|
-
# @return [Integer] true if within bounds
|
85
|
-
def within_bounds?(subject, options = {})
|
86
|
-
!exceeded?(subject, options)
|
87
|
-
end
|
88
|
-
|
89
|
-
# Execute a block once the rate limit is within bounds
|
90
|
-
# *WARNING* This will block the current thread until the rate limit is within bounds.
|
91
|
-
#
|
92
|
-
# @param [String] subject Subject for this rate limit
|
93
|
-
# @param [Hash] options Options hash
|
94
|
-
# @option options [Integer] :interval How far back to retrieve activity.
|
95
|
-
# @option options [Integer] :threshold Maximum number of actions
|
96
|
-
# @yield The block to be run
|
97
|
-
#
|
98
|
-
# @example Send an email as long as we haven't send 5 in the last 10 minutes
|
99
|
-
# RedisThrottler.exec_with_threshold(email, [:threshold => 5, :interval => 600]) do
|
100
|
-
# send_another_email
|
101
|
-
# end
|
102
|
-
def exec_within_threshold(subject, options = {}, &block)
|
103
|
-
options[:threshold] ||= 30
|
104
|
-
options[:interval] ||= 30
|
105
|
-
while exceeded?(subject, options)
|
106
|
-
sleep @bucket_interval
|
107
|
-
end
|
108
|
-
yield(self)
|
109
|
-
end
|
110
|
-
|
111
|
-
private
|
112
|
-
|
113
|
-
def get_bucket(time = Time.now.to_i)
|
114
|
-
((time % @bucket_span) / @bucket_interval).floor
|
115
|
-
end
|
116
|
-
|
117
|
-
def redis
|
118
|
-
@redis ||= Redis.new(host: '192.168.99.100', port: 32771)
|
5
|
+
module RedisThrottler
|
6
|
+
def self.included(base)
|
7
|
+
base.include(RedisThrottler::Model)
|
119
8
|
end
|
120
9
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/throttler_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
describe RedisThrottler do
|
4
4
|
|
5
5
|
before do
|
6
|
-
@rl = RedisThrottler.new('test')
|
6
|
+
@rl = RedisThrottler::Base.new('test')
|
7
7
|
@rl.send(:redis).flushdb
|
8
8
|
end
|
9
9
|
|
@@ -13,13 +13,13 @@ describe RedisThrottler do
|
|
13
13
|
|
14
14
|
it 'should not allow bucket count less than 3' do
|
15
15
|
expect do
|
16
|
-
RedisThrottler.new('test', {:bucket_span => 1, :bucket_interval => 1})
|
16
|
+
RedisThrottler::Base.new('test', {:bucket_span => 1, :bucket_interval => 1})
|
17
17
|
end.to raise_error(ArgumentError)
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'should not allow bucket expiry to be larger than the bucket span' do
|
21
21
|
expect do
|
22
|
-
RedisThrottler.new("key", {:bucket_expiry => 1200})
|
22
|
+
RedisThrottler::Base.new("key", {:bucket_expiry => 1200})
|
23
23
|
end.to raise_error(ArgumentError)
|
24
24
|
end
|
25
25
|
|
@@ -94,7 +94,7 @@ describe RedisThrottler do
|
|
94
94
|
# end
|
95
95
|
|
96
96
|
it 'counts correclty if bucket_span equals count-interval ' do
|
97
|
-
@rl = RedisThrottler.new('key', {:bucket_span => 10, bucket_interval: 1})
|
97
|
+
@rl = RedisThrottler::Base.new('key', {:bucket_span => 10, bucket_interval: 1})
|
98
98
|
@rl.add('value1')
|
99
99
|
expect(@rl.count('value1', 10)).to eql(1)
|
100
100
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-throttler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evan Surdam
|
@@ -114,6 +114,7 @@ files:
|
|
114
114
|
- Rakefile
|
115
115
|
- gemspec.yml
|
116
116
|
- lib/redis-throttler.rb
|
117
|
+
- lib/redis-throttler/base.rb
|
117
118
|
- lib/redis-throttler/model.rb
|
118
119
|
- lib/redis-throttler/version.rb
|
119
120
|
- redis-throttler.gemspec
|