pause 0.0.6 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/Gemfile +8 -0
- data/Rakefile +5 -0
- data/lib/pause.rb +14 -6
- data/lib/pause/action.rb +2 -6
- data/lib/pause/analyzer.rb +2 -2
- data/lib/pause/configuration.rb +5 -1
- data/lib/pause/logger.rb +11 -0
- data/lib/pause/redis/adapter.rb +40 -25
- data/lib/pause/redis/sharded_adapter.rb +17 -0
- data/lib/pause/version.rb +1 -1
- data/pause.gemspec +0 -6
- data/spec/pause/action_spec.rb +60 -53
- data/spec/pause/analyzer_spec.rb +6 -6
- data/spec/pause/configuration_spec.rb +10 -10
- data/spec/pause/pause_spec.rb +29 -0
- data/spec/pause/redis/adapter_spec.rb +81 -47
- data/spec/pause/redis/sharded_adapter_spec.rb +24 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/fakeredis.rb +0 -1
- metadata +9 -75
- data/.pairs +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9d25d869302d10074000e45ae903a1d28d66ccc
|
4
|
+
data.tar.gz: d3bcfbce3b73826e20481aa28f6a1a70dc61f172
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 812f85e93696fb48483f3d9096a9a6406509361a5145e3cd66614b57bb83e879ec4aa3fd749876a842966a2dd73a54344b0ea957d8e75ffd0bb0653c72aa9670
|
7
|
+
data.tar.gz: 29d51c794c8776748fa282ce63c308db83cc6092cbc6f0e407bb37b76140fa9fcc7bcd34847ecbc290f9e5f1b8e022b91e2f3ad17f700dfb5802c7aadf5e867c
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Rakefile
CHANGED
data/lib/pause.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'redis'
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
2
|
+
require 'pause/version'
|
3
|
+
require 'pause/configuration'
|
4
|
+
require 'pause/action'
|
5
|
+
require 'pause/analyzer'
|
6
|
+
require 'pause/logger'
|
7
|
+
require 'pause/redis/adapter'
|
8
|
+
require 'pause/redis/sharded_adapter'
|
7
9
|
require 'pause/rate_limited_event'
|
8
10
|
|
9
11
|
module Pause
|
@@ -25,7 +27,13 @@ module Pause
|
|
25
27
|
end
|
26
28
|
|
27
29
|
def adapter
|
28
|
-
@adapter ||=
|
30
|
+
@adapter ||= config.sharded ?
|
31
|
+
Pause::Redis::ShardedAdapter.new(config) :
|
32
|
+
Pause::Redis::Adapter.new(config)
|
33
|
+
end
|
34
|
+
|
35
|
+
def adapter=(adapter)
|
36
|
+
@adapter = adapter
|
29
37
|
end
|
30
38
|
|
31
39
|
def configure(&block)
|
data/lib/pause/action.rb
CHANGED
@@ -58,7 +58,7 @@ module Pause
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def increment!(count = 1, timestamp = Time.now.to_i)
|
61
|
-
adapter.increment(
|
61
|
+
adapter.increment(scope, identifier, timestamp, count)
|
62
62
|
end
|
63
63
|
|
64
64
|
def rate_limited?
|
@@ -68,7 +68,7 @@ module Pause
|
|
68
68
|
def ok?
|
69
69
|
Pause.analyzer.check(self).nil?
|
70
70
|
rescue ::Redis::CannotConnectError => e
|
71
|
-
|
71
|
+
Pause::Logger.fatal "Error connecting to redis: #{e.inspect} #{e.message} #{e.backtrace.join("\n")}"
|
72
72
|
false
|
73
73
|
end
|
74
74
|
|
@@ -92,10 +92,6 @@ module Pause
|
|
92
92
|
adapter.delete_rate_limited_key(scope, identifier)
|
93
93
|
end
|
94
94
|
|
95
|
-
def key
|
96
|
-
"#{self.scope}:#{identifier}"
|
97
|
-
end
|
98
|
-
|
99
95
|
# Actions can be globally disabled or re-enabled in a persistent
|
100
96
|
# way.
|
101
97
|
#
|
data/lib/pause/analyzer.rb
CHANGED
@@ -6,14 +6,14 @@ module Pause
|
|
6
6
|
|
7
7
|
def check(action)
|
8
8
|
timestamp = period_marker(Pause.config.resolution, Time.now.to_i)
|
9
|
-
set = adapter.key_history(action.
|
9
|
+
set = adapter.key_history(action.scope, action.identifier)
|
10
10
|
action.checks.each do |period_check|
|
11
11
|
start_time = timestamp - period_check.period_seconds
|
12
12
|
set.reverse.inject(0) do |sum, element|
|
13
13
|
break if element.ts < start_time
|
14
14
|
sum += element.count
|
15
15
|
if sum >= period_check.max_allowed
|
16
|
-
adapter.rate_limit!(action.
|
16
|
+
adapter.rate_limit!(action.scope, action.identifier, period_check.block_ttl)
|
17
17
|
# Note that Time.now is different from period_marker(resolution, Time.now), which
|
18
18
|
# rounds down to the nearest (resolution) seconds
|
19
19
|
return Pause::RateLimitedEvent.new(action, period_check, sum, Time.now.to_i)
|
data/lib/pause/configuration.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Pause
|
2
2
|
class Configuration
|
3
|
-
attr_writer :redis_host, :redis_port, :redis_db, :resolution, :history
|
3
|
+
attr_writer :redis_host, :redis_port, :redis_db, :resolution, :history, :sharded
|
4
4
|
|
5
5
|
def configure
|
6
6
|
yield self
|
@@ -26,5 +26,9 @@ module Pause
|
|
26
26
|
def history
|
27
27
|
(@history || 86400).to_i
|
28
28
|
end
|
29
|
+
|
30
|
+
def sharded
|
31
|
+
@sharded || false
|
32
|
+
end
|
29
33
|
end
|
30
34
|
end
|
data/lib/pause/logger.rb
ADDED
data/lib/pause/redis/adapter.rb
CHANGED
@@ -15,8 +15,8 @@ module Pause
|
|
15
15
|
@history = config.history
|
16
16
|
end
|
17
17
|
|
18
|
-
def increment(
|
19
|
-
k =
|
18
|
+
def increment(scope, identifier, timestamp, count = 1)
|
19
|
+
k = tracked_key(scope, identifier)
|
20
20
|
redis.multi do |redis|
|
21
21
|
redis.zincrby k, count, period_marker(resolution, timestamp)
|
22
22
|
redis.expire k, history
|
@@ -29,40 +29,47 @@ module Pause
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
def key_history(
|
33
|
-
extract_set_elements(
|
32
|
+
def key_history(scope, identifier)
|
33
|
+
extract_set_elements(tracked_key(scope, identifier))
|
34
34
|
end
|
35
35
|
|
36
|
-
def rate_limit!(
|
37
|
-
|
36
|
+
def rate_limit!(scope, identifier, block_ttl)
|
37
|
+
timestamp = Time.now.to_i + block_ttl
|
38
|
+
redis.zadd rate_limited_list(scope), timestamp, identifier
|
39
|
+
expire_block_list(scope)
|
38
40
|
end
|
39
41
|
|
40
|
-
def rate_limited?(
|
41
|
-
|
42
|
+
def rate_limited?(scope, identifier)
|
43
|
+
blocked_until = redis.zscore(rate_limited_list(scope), identifier)
|
44
|
+
!!blocked_until && blocked_until > Time.now.to_i
|
42
45
|
end
|
43
46
|
|
44
47
|
def all_keys(scope)
|
45
|
-
keys(
|
48
|
+
keys(tracked_scope(scope))
|
46
49
|
end
|
47
50
|
|
48
51
|
def rate_limited_keys(scope)
|
49
|
-
|
52
|
+
redis.zrangebyscore rate_limited_list(scope), Time.now.to_i, '+inf'
|
50
53
|
end
|
51
54
|
|
55
|
+
# For a scope, delete the entire sorted set that holds the block list.
|
56
|
+
# Also delete the original tracking information, so we don't immediately re-block the id
|
52
57
|
def delete_rate_limited_keys(scope)
|
53
|
-
|
58
|
+
delete_tracking_keys(scope, rate_limited_keys(scope))
|
59
|
+
redis.del rate_limited_list(scope)
|
54
60
|
end
|
55
61
|
|
56
62
|
def delete_rate_limited_key(scope, id)
|
57
|
-
|
63
|
+
delete_tracking_keys(scope, [id])
|
64
|
+
redis.zrem rate_limited_list(scope), id
|
58
65
|
end
|
59
66
|
|
60
67
|
def disable(scope)
|
61
|
-
redis.set("
|
68
|
+
redis.set("internal:|#{scope}|:disabled", "1")
|
62
69
|
end
|
63
70
|
|
64
71
|
def enable(scope)
|
65
|
-
redis.del("
|
72
|
+
redis.del("internal:|#{scope}|:disabled")
|
66
73
|
end
|
67
74
|
|
68
75
|
def disabled?(scope)
|
@@ -70,15 +77,18 @@ module Pause
|
|
70
77
|
end
|
71
78
|
|
72
79
|
def enabled?(scope)
|
73
|
-
redis.
|
80
|
+
redis.get("internal:|#{scope}|:disabled").nil?
|
81
|
+
end
|
82
|
+
|
83
|
+
def expire_block_list(scope)
|
84
|
+
redis.zremrangebyscore rate_limited_list(scope), '-inf', Time.now.to_i
|
74
85
|
end
|
75
86
|
|
76
87
|
private
|
77
88
|
|
78
|
-
def
|
79
|
-
increment_keys = ids.map{ |key|
|
80
|
-
|
81
|
-
redis.del(increment_keys + rate_limited_keys)
|
89
|
+
def delete_tracking_keys(scope, ids)
|
90
|
+
increment_keys = ids.map{ |key| tracked_key(scope, key) }
|
91
|
+
redis.del(increment_keys)
|
82
92
|
end
|
83
93
|
|
84
94
|
def redis
|
@@ -87,22 +97,27 @@ module Pause
|
|
87
97
|
db: Pause.config.redis_db)
|
88
98
|
end
|
89
99
|
|
90
|
-
def
|
91
|
-
["i", scope
|
100
|
+
def tracked_scope(scope)
|
101
|
+
["i", scope].join(':')
|
102
|
+
end
|
103
|
+
|
104
|
+
def tracked_key(scope, identifier)
|
105
|
+
id = "|#{identifier}|"
|
106
|
+
[tracked_scope(scope), id].join(':')
|
92
107
|
end
|
93
108
|
|
94
|
-
def
|
95
|
-
|
109
|
+
def rate_limited_list(scope)
|
110
|
+
"b:|#{scope}|"
|
96
111
|
end
|
97
112
|
|
98
113
|
def keys(key_scope)
|
99
114
|
redis.keys("#{key_scope}:*").map do |key|
|
100
|
-
key.gsub(/^#{key_scope}:/, "")
|
115
|
+
key.gsub(/^#{key_scope}:/, "").tr('|','')
|
101
116
|
end
|
102
117
|
end
|
103
118
|
|
104
119
|
def extract_set_elements(key)
|
105
|
-
(redis.zrange key, 0, -1, :
|
120
|
+
(redis.zrange key, 0, -1, with_scores: true).map do |slice|
|
106
121
|
Pause::SetElement.new(slice[0].to_i, slice[1].to_i)
|
107
122
|
end.sort
|
108
123
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pause
|
2
|
+
module Redis
|
3
|
+
class OperationNotSupported < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
# This class encapsulates Redis operations used by Pause.
|
7
|
+
# Operations that are not possible when data is sharded
|
8
|
+
# raise an error.
|
9
|
+
class ShardedAdapter < Adapter
|
10
|
+
private
|
11
|
+
|
12
|
+
def keys(_key_scope)
|
13
|
+
raise OperationNotSupported.new("Can not be executed when Pause is configured in sharded mode")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/pause/version.rb
CHANGED
data/pause.gemspec
CHANGED
@@ -18,10 +18,4 @@ Gem::Specification.new do |gem|
|
|
18
18
|
gem.require_paths = ["lib"]
|
19
19
|
|
20
20
|
gem.add_dependency 'redis'
|
21
|
-
|
22
|
-
gem.add_development_dependency 'rspec'
|
23
|
-
gem.add_development_dependency 'fakeredis'
|
24
|
-
gem.add_development_dependency 'timecop'
|
25
|
-
gem.add_development_dependency 'guard-rspec'
|
26
|
-
gem.add_development_dependency 'rb-fsevent'
|
27
21
|
end
|
data/spec/pause/action_spec.rb
CHANGED
@@ -15,9 +15,10 @@ describe Pause::Action do
|
|
15
15
|
let(:configuration) { Pause::Configuration.new }
|
16
16
|
|
17
17
|
before do
|
18
|
-
Pause.
|
19
|
-
Pause.config.
|
20
|
-
Pause.config.
|
18
|
+
allow(Pause).to receive(:config).and_return(configuration)
|
19
|
+
allow(Pause.config).to receive(:resolution).and_return(resolution)
|
20
|
+
allow(Pause.config).to receive(:history).and_return(history)
|
21
|
+
allow(Pause).to receive(:adapter).and_return(Pause::Redis::Adapter.new(Pause.config))
|
21
22
|
end
|
22
23
|
|
23
24
|
let(:action) { MyNotification.new("1237612") }
|
@@ -27,7 +28,7 @@ describe Pause::Action do
|
|
27
28
|
it "should increment" do
|
28
29
|
time = Time.now
|
29
30
|
Timecop.freeze time do
|
30
|
-
Pause.adapter.
|
31
|
+
expect(Pause.adapter).to receive(:increment).with(action.scope, '1237612', time.to_i, 1)
|
31
32
|
action.increment!
|
32
33
|
end
|
33
34
|
end
|
@@ -39,41 +40,47 @@ describe Pause::Action do
|
|
39
40
|
Timecop.freeze time do
|
40
41
|
4.times do
|
41
42
|
action.increment!
|
42
|
-
action.ok
|
43
|
+
expect(action.ok?).to be true
|
43
44
|
end
|
44
45
|
action.increment!
|
45
|
-
action.ok
|
46
|
+
expect(action.ok?).to be false
|
46
47
|
end
|
47
48
|
end
|
48
49
|
|
49
50
|
it "should successfully consider different period checks" do
|
50
|
-
time =
|
51
|
-
|
52
|
-
action.increment! 4, time - 25
|
53
|
-
action.ok?.should be_true
|
51
|
+
time = Time.parse('Sept 22, 11:34:00')
|
54
52
|
|
55
|
-
|
56
|
-
|
53
|
+
Timecop.freeze time - 30 do
|
54
|
+
action.increment! 4
|
55
|
+
expect(action.ok?).to be true
|
56
|
+
end
|
57
57
|
|
58
|
-
|
58
|
+
Timecop.freeze time do
|
59
|
+
action.increment! 2
|
60
|
+
expect(action.ok?).to be true
|
61
|
+
end
|
59
62
|
|
60
|
-
|
63
|
+
Timecop.freeze time do
|
64
|
+
action.increment! 1
|
65
|
+
expect(action.ok?).to be false
|
66
|
+
end
|
61
67
|
end
|
62
68
|
|
63
69
|
it "should return false and silently fail if redis is not available" do
|
64
|
-
|
70
|
+
allow(Pause::Logger).to receive(:fatal)
|
71
|
+
allow_any_instance_of(Redis).to receive(:zrange).and_raise Redis::CannotConnectError
|
65
72
|
time = period_marker(resolution, Time.now.to_i)
|
66
73
|
|
67
74
|
action.increment! 4, time - 25
|
68
75
|
|
69
|
-
action.ok
|
76
|
+
expect(action.ok?).to be false
|
70
77
|
end
|
71
78
|
end
|
72
79
|
|
73
80
|
describe "#analyze" do
|
74
81
|
context "action should not be rate limited" do
|
75
82
|
it "returns nil" do
|
76
|
-
action.analyze.
|
83
|
+
expect(action.analyze).to be nil
|
77
84
|
end
|
78
85
|
end
|
79
86
|
|
@@ -89,11 +96,11 @@ describe Pause::Action do
|
|
89
96
|
|
90
97
|
expected_rate_limit = Pause::RateLimitedEvent.new(action, action.checks[0], 7, time.to_i)
|
91
98
|
|
92
|
-
rate_limit.
|
93
|
-
rate_limit.identifier.
|
94
|
-
rate_limit.sum.
|
95
|
-
rate_limit.period_check.
|
96
|
-
rate_limit.timestamp.
|
99
|
+
expect(rate_limit).to be_a(Pause::RateLimitedEvent)
|
100
|
+
expect(rate_limit.identifier).to eq(expected_rate_limit.identifier)
|
101
|
+
expect(rate_limit.sum).to eq(expected_rate_limit.sum)
|
102
|
+
expect(rate_limit.period_check).to eq(expected_rate_limit.period_check)
|
103
|
+
expect(rate_limit.timestamp).to eq(expected_rate_limit.timestamp)
|
97
104
|
end
|
98
105
|
end
|
99
106
|
end
|
@@ -106,8 +113,8 @@ describe Pause::Action do
|
|
106
113
|
action.ok?
|
107
114
|
other_action.ok?
|
108
115
|
|
109
|
-
MyNotification.tracked_identifiers.
|
110
|
-
MyNotification.tracked_identifiers.
|
116
|
+
expect(MyNotification.tracked_identifiers).to include(action.identifier)
|
117
|
+
expect(MyNotification.tracked_identifiers).to include(other_action.identifier)
|
111
118
|
end
|
112
119
|
end
|
113
120
|
|
@@ -119,8 +126,8 @@ describe Pause::Action do
|
|
119
126
|
action.ok?
|
120
127
|
other_action.ok?
|
121
128
|
|
122
|
-
MyNotification.rate_limited_identifiers.
|
123
|
-
MyNotification.rate_limited_identifiers.
|
129
|
+
expect(MyNotification.rate_limited_identifiers).to include(action.identifier)
|
130
|
+
expect(MyNotification.rate_limited_identifiers).to include(other_action.identifier)
|
124
131
|
end
|
125
132
|
end
|
126
133
|
|
@@ -132,13 +139,13 @@ describe Pause::Action do
|
|
132
139
|
action.ok?
|
133
140
|
other_action.ok?
|
134
141
|
|
135
|
-
MyNotification.tracked_identifiers.
|
136
|
-
MyNotification.rate_limited_identifiers.
|
142
|
+
expect(MyNotification.tracked_identifiers).to include(action.identifier, other_action.identifier)
|
143
|
+
expect(MyNotification.rate_limited_identifiers).to eq([action.identifier])
|
137
144
|
|
138
145
|
MyNotification.unblock_all
|
139
146
|
|
140
|
-
MyNotification.rate_limited_identifiers.
|
141
|
-
MyNotification.tracked_identifiers.
|
147
|
+
expect(MyNotification.rate_limited_identifiers).to be_empty
|
148
|
+
expect(MyNotification.tracked_identifiers).to eq([other_action.identifier])
|
142
149
|
end
|
143
150
|
end
|
144
151
|
|
@@ -146,11 +153,11 @@ describe Pause::Action do
|
|
146
153
|
it 'unblocks the specified id' do
|
147
154
|
10.times { action.increment! }
|
148
155
|
|
149
|
-
expect(action.ok?).to
|
156
|
+
expect(action.ok?).to be false
|
150
157
|
|
151
158
|
action.unblock
|
152
159
|
|
153
|
-
expect(action.ok?).to
|
160
|
+
expect(action.ok?).to be true
|
154
161
|
end
|
155
162
|
end
|
156
163
|
end
|
@@ -171,23 +178,23 @@ describe Pause::Action, ".check" do
|
|
171
178
|
end
|
172
179
|
|
173
180
|
it "should define a period check on new instances" do
|
174
|
-
ActionWithCheck.new("id").checks.
|
175
|
-
|
176
|
-
|
181
|
+
expect(ActionWithCheck.new("id").checks).to eq([
|
182
|
+
Pause::PeriodCheck.new(100, 150, 200)
|
183
|
+
])
|
177
184
|
end
|
178
185
|
|
179
186
|
it "should define a period check on new instances" do
|
180
|
-
ActionWithMultipleChecks.new("id").checks.
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
187
|
+
expect(ActionWithMultipleChecks.new("id").checks).to eq([
|
188
|
+
Pause::PeriodCheck.new(100, 150, 200),
|
189
|
+
Pause::PeriodCheck.new(200, 150, 200),
|
190
|
+
Pause::PeriodCheck.new(300, 150, 200)
|
191
|
+
])
|
185
192
|
end
|
186
193
|
|
187
194
|
it "should accept hash arguments" do
|
188
|
-
ActionWithHashChecks.new("id").checks.
|
189
|
-
|
190
|
-
|
195
|
+
expect(ActionWithHashChecks.new("id").checks).to eq([
|
196
|
+
Pause::PeriodCheck.new(50, 100, 60)
|
197
|
+
])
|
191
198
|
end
|
192
199
|
|
193
200
|
end
|
@@ -197,9 +204,9 @@ describe Pause::Action, ".scope" do
|
|
197
204
|
end
|
198
205
|
|
199
206
|
it "should raise if scope is not defined" do
|
200
|
-
|
207
|
+
expect {
|
201
208
|
UndefinedScopeAction.new("1.2.3.4").scope
|
202
|
-
}.
|
209
|
+
}.to raise_error("Should implement scope. (Ex: ipn:follow)")
|
203
210
|
end
|
204
211
|
|
205
212
|
class DefinedScopeAction < Pause::Action
|
@@ -207,7 +214,7 @@ describe Pause::Action, ".scope" do
|
|
207
214
|
end
|
208
215
|
|
209
216
|
it "should set scope on class" do
|
210
|
-
DefinedScopeAction.new("1.2.3.4").scope.
|
217
|
+
expect(DefinedScopeAction.new("1.2.3.4").scope).to eq("my:scope")
|
211
218
|
end
|
212
219
|
end
|
213
220
|
|
@@ -228,27 +235,27 @@ describe Pause::Action, "enabled/disabled states" do
|
|
228
235
|
|
229
236
|
describe "#disable" do
|
230
237
|
before do
|
231
|
-
action.
|
232
|
-
action.
|
238
|
+
expect(action).to be_enabled
|
239
|
+
expect(action).to_not be_disabled
|
233
240
|
action.disable
|
234
241
|
end
|
235
242
|
|
236
243
|
it "disables the action" do
|
237
|
-
action.
|
238
|
-
action.
|
244
|
+
expect(action).to be_disabled
|
245
|
+
expect(action).to_not be_enabled
|
239
246
|
end
|
240
247
|
end
|
241
248
|
|
242
249
|
describe "#enable" do
|
243
250
|
before do
|
244
251
|
action.disable
|
245
|
-
action.
|
252
|
+
expect(action).to_not be_enabled
|
246
253
|
action.enable
|
247
254
|
end
|
248
255
|
|
249
256
|
it "enables the action" do
|
250
|
-
action.
|
251
|
-
action.
|
257
|
+
expect(action).to be_enabled
|
258
|
+
expect(action).to_not be_disabled
|
252
259
|
end
|
253
260
|
end
|
254
261
|
end
|
data/spec/pause/analyzer_spec.rb
CHANGED
@@ -15,9 +15,9 @@ describe Pause::Analyzer do
|
|
15
15
|
let(:configuration) { Pause::Configuration.new }
|
16
16
|
|
17
17
|
before do
|
18
|
-
Pause.
|
19
|
-
Pause.config.
|
20
|
-
Pause.config.
|
18
|
+
allow(Pause).to receive(:config).and_return(configuration)
|
19
|
+
allow(Pause.config).to receive(:resolution).and_return(resolution)
|
20
|
+
allow(Pause.config).to receive(:history).and_return(history)
|
21
21
|
end
|
22
22
|
|
23
23
|
let(:analyzer) { Pause.analyzer }
|
@@ -27,7 +27,7 @@ describe Pause::Analyzer do
|
|
27
27
|
describe "#analyze" do
|
28
28
|
it "checks and blocks if max_allowed is reached" do
|
29
29
|
time = Time.now
|
30
|
-
adapter.
|
30
|
+
expect(adapter).to receive(:rate_limit!).once.with(action.scope, '1243123', 12)
|
31
31
|
Timecop.freeze time do
|
32
32
|
5.times do
|
33
33
|
action.increment!
|
@@ -39,7 +39,7 @@ describe Pause::Analyzer do
|
|
39
39
|
|
40
40
|
describe "#check" do
|
41
41
|
it "should return nil if action is NOT blocked" do
|
42
|
-
analyzer.check(action).
|
42
|
+
expect(analyzer.check(action)).to be nil
|
43
43
|
end
|
44
44
|
|
45
45
|
it "should return blocked action if action is blocked" do
|
@@ -47,7 +47,7 @@ describe Pause::Analyzer do
|
|
47
47
|
5.times do
|
48
48
|
action.increment!
|
49
49
|
end
|
50
|
-
analyzer.check(action).
|
50
|
+
expect(analyzer.check(action)).to be_a(Pause::RateLimitedEvent)
|
51
51
|
end
|
52
52
|
end
|
53
53
|
end
|
@@ -14,12 +14,12 @@ describe Pause::Configuration, "#configure" do
|
|
14
14
|
c.history = 6000
|
15
15
|
end
|
16
16
|
|
17
|
-
subject.redis_host.
|
18
|
-
subject.redis_port.
|
19
|
-
subject.redis_db.
|
17
|
+
expect(subject.redis_host).to eq("128.23.12.8")
|
18
|
+
expect(subject.redis_port).to eq(2134)
|
19
|
+
expect(subject.redis_db).to eq("13")
|
20
20
|
|
21
|
-
subject.resolution.
|
22
|
-
subject.history.
|
21
|
+
expect(subject.resolution).to eq(5000)
|
22
|
+
expect(subject.history).to eq(6000)
|
23
23
|
end
|
24
24
|
|
25
25
|
it "should provide redis defaults" do
|
@@ -27,10 +27,10 @@ describe Pause::Configuration, "#configure" do
|
|
27
27
|
# do nothing
|
28
28
|
end
|
29
29
|
|
30
|
-
subject.redis_host.
|
31
|
-
subject.redis_port.
|
32
|
-
subject.redis_db.
|
33
|
-
subject.resolution.
|
34
|
-
subject.history.
|
30
|
+
expect(subject.redis_host).to eq("127.0.0.1")
|
31
|
+
expect(subject.redis_port).to eq(6379)
|
32
|
+
expect(subject.redis_db).to eq("1")
|
33
|
+
expect(subject.resolution).to eq(600) # 10 minutes
|
34
|
+
expect(subject.history).to eq(86400) # one day
|
35
35
|
end
|
36
36
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Pause do
|
4
|
+
describe 'adapter' do
|
5
|
+
let(:configuration) { Pause::Configuration.new }
|
6
|
+
|
7
|
+
before do
|
8
|
+
Pause.adapter = nil
|
9
|
+
allow(Pause).to receive(:config).and_return(configuration)
|
10
|
+
configuration.configure { |c| c.sharded = sharded }
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'pause is sharded' do
|
14
|
+
let(:sharded) { true }
|
15
|
+
|
16
|
+
it 'is a ShardedAdapter' do
|
17
|
+
expect(Pause.adapter).to be_a(Pause::Redis::ShardedAdapter)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'pause is not sharded' do
|
22
|
+
let(:sharded) { false }
|
23
|
+
|
24
|
+
it 'is an Adapter' do
|
25
|
+
expect(Pause.adapter).to be_a(Pause::Redis::Adapter)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -9,9 +9,9 @@ describe Pause::Redis::Adapter do
|
|
9
9
|
let(:configuration) { Pause::Configuration.new }
|
10
10
|
|
11
11
|
before do
|
12
|
-
Pause.
|
13
|
-
Pause.config.
|
14
|
-
Pause.config.
|
12
|
+
allow(Pause).to receive(:config).and_return(configuration)
|
13
|
+
allow(Pause.config).to receive(:resolution).and_return(resolution)
|
14
|
+
allow(Pause.config).to receive(:history).and_return(history)
|
15
15
|
redis_conn.flushall
|
16
16
|
end
|
17
17
|
|
@@ -19,116 +19,150 @@ describe Pause::Redis::Adapter do
|
|
19
19
|
let(:redis_conn) { adapter.send(:redis) }
|
20
20
|
|
21
21
|
describe '#increment' do
|
22
|
-
let(:
|
22
|
+
let(:scope) { "blah" }
|
23
|
+
let(:identifier) { "213213" }
|
24
|
+
let(:tracked_key) { "i:blah:|213213|"}
|
23
25
|
|
24
26
|
it "should add key to a redis set" do
|
25
|
-
adapter.increment(
|
26
|
-
set = redis_conn.zrange(
|
27
|
-
set.
|
28
|
-
set.size.
|
29
|
-
set[0].size.
|
27
|
+
adapter.increment(scope, identifier, Time.now.to_i)
|
28
|
+
set = redis_conn.zrange(tracked_key, 0, -1, :with_scores => true)
|
29
|
+
expect(set).to_not be_empty
|
30
|
+
expect(set.size).to eql(1)
|
31
|
+
expect(set[0].size).to eql(2)
|
30
32
|
end
|
31
33
|
|
32
34
|
it "should remove old key from a redis set" do
|
33
35
|
time = Time.now
|
34
|
-
redis_conn.
|
36
|
+
expect(redis_conn).to receive(:zrem).with(tracked_key, [adapter.period_marker(resolution, time)])
|
35
37
|
|
36
38
|
adapter.time_blocks_to_keep = 1
|
37
39
|
Timecop.freeze time do
|
38
|
-
adapter.increment(
|
40
|
+
adapter.increment(scope, identifier, Time.now.to_i)
|
39
41
|
end
|
40
42
|
Timecop.freeze time + (adapter.resolution + 1) do
|
41
|
-
adapter.increment(
|
43
|
+
adapter.increment(scope, identifier, Time.now.to_i)
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
45
47
|
it "sets expiry on key" do
|
46
|
-
redis_conn.
|
47
|
-
adapter.increment(
|
48
|
+
expect(redis_conn).to receive(:expire).with(tracked_key, history)
|
49
|
+
adapter.increment(scope, identifier, Time.now.to_i)
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
51
|
-
describe
|
52
|
-
let(:
|
53
|
-
let(:
|
54
|
-
let(:
|
53
|
+
describe '#expire_block_list' do
|
54
|
+
let(:scope) { 'a' }
|
55
|
+
let(:expired_identifier) { '123' }
|
56
|
+
let(:blocked_identifier) { '124' }
|
57
|
+
|
58
|
+
it 'clears all entries with score older than now' do
|
59
|
+
now = Time.now
|
60
|
+
|
61
|
+
Timecop.freeze now - 10 do
|
62
|
+
adapter.rate_limit!(scope, expired_identifier, 5)
|
63
|
+
end
|
64
|
+
|
65
|
+
Timecop.freeze now - 4 do
|
66
|
+
adapter.rate_limit!(scope, blocked_identifier, 5)
|
67
|
+
end
|
55
68
|
|
56
|
-
|
57
|
-
|
58
|
-
redis_conn.
|
59
|
-
redis_conn.
|
69
|
+
adapter.expire_block_list(scope)
|
70
|
+
|
71
|
+
expect(redis_conn.zscore('b:|a|', blocked_identifier)).not_to be nil
|
72
|
+
expect(redis_conn.zscore('b:|a|', expired_identifier)).to be nil
|
60
73
|
end
|
61
74
|
end
|
62
75
|
|
63
|
-
describe "#
|
64
|
-
|
76
|
+
describe "#rate_limit!" do
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#rate_limited?" do
|
80
|
+
let(:scope) { 'ipn:follow' }
|
81
|
+
let(:identifier) { '123461234' }
|
65
82
|
let(:blocked_key) { "b:#{key}" }
|
66
83
|
let(:ttl) { 110000 }
|
67
84
|
|
68
85
|
it "should return true if blocked" do
|
69
|
-
adapter.rate_limit!(
|
70
|
-
(
|
86
|
+
adapter.rate_limit!(scope, identifier, ttl)
|
87
|
+
expect(adapter.rate_limited?(scope, identifier)).to be true
|
71
88
|
end
|
72
89
|
end
|
73
90
|
|
74
|
-
describe "#
|
91
|
+
describe "#tracked_key" do
|
75
92
|
it "prefixes key" do
|
76
|
-
adapter.send(:
|
93
|
+
expect(adapter.send(:tracked_key, "abc", "12345")).to eq("i:abc:|12345|")
|
77
94
|
end
|
78
95
|
end
|
79
96
|
|
80
97
|
describe '#enable' do
|
81
98
|
it 'deletes the disabled flag in redis' do
|
82
99
|
adapter.disable("boom")
|
83
|
-
expect(adapter.disabled?("boom")).to
|
100
|
+
expect(adapter.disabled?("boom")).to be true
|
84
101
|
adapter.enable("boom")
|
85
|
-
expect(adapter.disabled?("boom")).to
|
102
|
+
expect(adapter.disabled?("boom")).to be false
|
86
103
|
end
|
87
104
|
end
|
88
105
|
|
89
106
|
describe '#disable' do
|
90
107
|
it 'sets the disabled flag in redis' do
|
91
|
-
expect(adapter.enabled?("boom")).to
|
108
|
+
expect(adapter.enabled?("boom")).to be true
|
92
109
|
adapter.disable("boom")
|
93
|
-
expect(adapter.enabled?("boom")).to
|
110
|
+
expect(adapter.enabled?("boom")).to be false
|
94
111
|
end
|
95
112
|
end
|
96
113
|
|
97
114
|
describe '#rate_limit!' do
|
98
115
|
it 'rate limits a key for a specific ttl' do
|
99
|
-
expect(adapter.rate_limited?('1')).to
|
100
|
-
adapter.rate_limit!('1', 10)
|
101
|
-
expect(adapter.rate_limited?('1')).to
|
116
|
+
expect(adapter.rate_limited?('blah', '1')).to be false
|
117
|
+
adapter.rate_limit!('blah', '1', 10)
|
118
|
+
expect(adapter.rate_limited?('blah', '1')).to be true
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'redis internals' do
|
122
|
+
let(:scope) { 'ipn:follow' }
|
123
|
+
let(:identifier) { '1234' }
|
124
|
+
let(:blocked_key) { "b:|#{scope}|" }
|
125
|
+
let(:ttl) { 110000 }
|
126
|
+
|
127
|
+
it "saves ip to redis with expiration" do
|
128
|
+
time = Time.now
|
129
|
+
Timecop.freeze time do
|
130
|
+
adapter.rate_limit!(scope, identifier, ttl)
|
131
|
+
end
|
132
|
+
expect(redis_conn.zscore(blocked_key, identifier)).to_not be nil
|
133
|
+
expect(redis_conn.zscore(blocked_key, identifier)).to eq(time.to_i + ttl)
|
134
|
+
end
|
135
|
+
|
102
136
|
end
|
103
137
|
end
|
104
138
|
|
105
139
|
describe '#delete_rate_limited_keys' do
|
106
140
|
it 'calls redis del with all keys' do
|
107
|
-
adapter.rate_limit!('boom
|
108
|
-
adapter.rate_limit!('boom
|
141
|
+
adapter.rate_limit!('boom', '1', 10)
|
142
|
+
adapter.rate_limit!('boom', '2', 10)
|
109
143
|
|
110
|
-
expect(adapter.rate_limited?('boom
|
111
|
-
expect(adapter.rate_limited?('boom
|
144
|
+
expect(adapter.rate_limited?('boom', '1')).to be true
|
145
|
+
expect(adapter.rate_limited?('boom', '2')).to be true
|
112
146
|
|
113
147
|
adapter.delete_rate_limited_keys('boom')
|
114
148
|
|
115
|
-
expect(adapter.rate_limited?('boom
|
116
|
-
expect(adapter.rate_limited?('boom
|
149
|
+
expect(adapter.rate_limited?('boom', '1')).to be false
|
150
|
+
expect(adapter.rate_limited?('boom', '2')).to be false
|
117
151
|
end
|
118
152
|
end
|
119
153
|
|
120
154
|
describe '#delete_rate_limit_key' do
|
121
155
|
it 'calls redis del with all keys' do
|
122
|
-
adapter.rate_limit!('boom
|
123
|
-
adapter.rate_limit!('boom
|
156
|
+
adapter.rate_limit!('boom', '1', 10)
|
157
|
+
adapter.rate_limit!('boom', '2', 10)
|
124
158
|
|
125
|
-
expect(adapter.rate_limited?('boom
|
126
|
-
expect(adapter.rate_limited?('boom
|
159
|
+
expect(adapter.rate_limited?('boom', '1')).to be true
|
160
|
+
expect(adapter.rate_limited?('boom', '2')).to be true
|
127
161
|
|
128
162
|
adapter.delete_rate_limited_key('boom', '1')
|
129
163
|
|
130
|
-
expect(adapter.rate_limited?('boom
|
131
|
-
expect(adapter.rate_limited?('boom
|
164
|
+
expect(adapter.rate_limited?('boom', '1')).to be false
|
165
|
+
expect(adapter.rate_limited?('boom', '2')).to be true
|
132
166
|
end
|
133
167
|
end
|
134
168
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'date'
|
3
|
+
require 'timecop'
|
4
|
+
|
5
|
+
describe Pause::Redis::ShardedAdapter do
|
6
|
+
|
7
|
+
let(:resolution) { 10 }
|
8
|
+
let(:history) { 60 }
|
9
|
+
let(:configuration) { Pause::Configuration.new }
|
10
|
+
|
11
|
+
before do
|
12
|
+
allow(Pause).to receive(:config).and_return(configuration)
|
13
|
+
allow(Pause.config).to receive(:resolution).and_return(resolution)
|
14
|
+
allow(Pause.config).to receive(:history).and_return(history)
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:adapter) { Pause::Redis::ShardedAdapter.new(Pause.config) }
|
18
|
+
|
19
|
+
describe '#all_keys' do
|
20
|
+
it 'is not supported' do
|
21
|
+
expect { adapter.all_keys('cake') }.to raise_error(Pause::Redis::OperationNotSupported)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -9,10 +9,10 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
|
|
9
9
|
require 'rubygems'
|
10
10
|
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
|
11
11
|
require 'pause'
|
12
|
+
require 'pry'
|
12
13
|
require 'support/fakeredis'
|
13
14
|
|
14
15
|
RSpec.configure do |config|
|
15
|
-
config.treat_symbols_as_metadata_keys_with_true_values = true
|
16
16
|
config.run_all_when_everything_filtered = true
|
17
17
|
config.filter_run :focus
|
18
18
|
|
data/spec/support/fakeredis.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pause
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Atasay Gokkaya
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date:
|
14
|
+
date: 2015-09-22 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: redis
|
@@ -27,76 +27,6 @@ dependencies:
|
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '0'
|
30
|
-
- !ruby/object:Gem::Dependency
|
31
|
-
name: rspec
|
32
|
-
requirement: !ruby/object:Gem::Requirement
|
33
|
-
requirements:
|
34
|
-
- - ">="
|
35
|
-
- !ruby/object:Gem::Version
|
36
|
-
version: '0'
|
37
|
-
type: :development
|
38
|
-
prerelease: false
|
39
|
-
version_requirements: !ruby/object:Gem::Requirement
|
40
|
-
requirements:
|
41
|
-
- - ">="
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: '0'
|
44
|
-
- !ruby/object:Gem::Dependency
|
45
|
-
name: fakeredis
|
46
|
-
requirement: !ruby/object:Gem::Requirement
|
47
|
-
requirements:
|
48
|
-
- - ">="
|
49
|
-
- !ruby/object:Gem::Version
|
50
|
-
version: '0'
|
51
|
-
type: :development
|
52
|
-
prerelease: false
|
53
|
-
version_requirements: !ruby/object:Gem::Requirement
|
54
|
-
requirements:
|
55
|
-
- - ">="
|
56
|
-
- !ruby/object:Gem::Version
|
57
|
-
version: '0'
|
58
|
-
- !ruby/object:Gem::Dependency
|
59
|
-
name: timecop
|
60
|
-
requirement: !ruby/object:Gem::Requirement
|
61
|
-
requirements:
|
62
|
-
- - ">="
|
63
|
-
- !ruby/object:Gem::Version
|
64
|
-
version: '0'
|
65
|
-
type: :development
|
66
|
-
prerelease: false
|
67
|
-
version_requirements: !ruby/object:Gem::Requirement
|
68
|
-
requirements:
|
69
|
-
- - ">="
|
70
|
-
- !ruby/object:Gem::Version
|
71
|
-
version: '0'
|
72
|
-
- !ruby/object:Gem::Dependency
|
73
|
-
name: guard-rspec
|
74
|
-
requirement: !ruby/object:Gem::Requirement
|
75
|
-
requirements:
|
76
|
-
- - ">="
|
77
|
-
- !ruby/object:Gem::Version
|
78
|
-
version: '0'
|
79
|
-
type: :development
|
80
|
-
prerelease: false
|
81
|
-
version_requirements: !ruby/object:Gem::Requirement
|
82
|
-
requirements:
|
83
|
-
- - ">="
|
84
|
-
- !ruby/object:Gem::Version
|
85
|
-
version: '0'
|
86
|
-
- !ruby/object:Gem::Dependency
|
87
|
-
name: rb-fsevent
|
88
|
-
requirement: !ruby/object:Gem::Requirement
|
89
|
-
requirements:
|
90
|
-
- - ">="
|
91
|
-
- !ruby/object:Gem::Version
|
92
|
-
version: '0'
|
93
|
-
type: :development
|
94
|
-
prerelease: false
|
95
|
-
version_requirements: !ruby/object:Gem::Requirement
|
96
|
-
requirements:
|
97
|
-
- - ">="
|
98
|
-
- !ruby/object:Gem::Version
|
99
|
-
version: '0'
|
100
30
|
description: Real time rate limiting for multi-process ruby environments based on
|
101
31
|
Redis
|
102
32
|
email:
|
@@ -109,7 +39,6 @@ extensions: []
|
|
109
39
|
extra_rdoc_files: []
|
110
40
|
files:
|
111
41
|
- ".gitignore"
|
112
|
-
- ".pairs"
|
113
42
|
- ".rspec"
|
114
43
|
- ".rvmrc"
|
115
44
|
- ".travis.yml"
|
@@ -123,14 +52,18 @@ files:
|
|
123
52
|
- lib/pause/analyzer.rb
|
124
53
|
- lib/pause/configuration.rb
|
125
54
|
- lib/pause/helper/timing.rb
|
55
|
+
- lib/pause/logger.rb
|
126
56
|
- lib/pause/rate_limited_event.rb
|
127
57
|
- lib/pause/redis/adapter.rb
|
58
|
+
- lib/pause/redis/sharded_adapter.rb
|
128
59
|
- lib/pause/version.rb
|
129
60
|
- pause.gemspec
|
130
61
|
- spec/pause/action_spec.rb
|
131
62
|
- spec/pause/analyzer_spec.rb
|
132
63
|
- spec/pause/configuration_spec.rb
|
64
|
+
- spec/pause/pause_spec.rb
|
133
65
|
- spec/pause/redis/adapter_spec.rb
|
66
|
+
- spec/pause/redis/sharded_adapter_spec.rb
|
134
67
|
- spec/spec_helper.rb
|
135
68
|
- spec/support/fakeredis.rb
|
136
69
|
homepage: https://github.com/wanelo/pause
|
@@ -152,7 +85,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
85
|
version: '0'
|
153
86
|
requirements: []
|
154
87
|
rubyforge_project:
|
155
|
-
rubygems_version: 2.2.
|
88
|
+
rubygems_version: 2.2.3
|
156
89
|
signing_key:
|
157
90
|
specification_version: 4
|
158
91
|
summary: RReal time rate limiting for multi-process ruby environments based on Redis
|
@@ -160,7 +93,8 @@ test_files:
|
|
160
93
|
- spec/pause/action_spec.rb
|
161
94
|
- spec/pause/analyzer_spec.rb
|
162
95
|
- spec/pause/configuration_spec.rb
|
96
|
+
- spec/pause/pause_spec.rb
|
163
97
|
- spec/pause/redis/adapter_spec.rb
|
98
|
+
- spec/pause/redis/sharded_adapter_spec.rb
|
164
99
|
- spec/spec_helper.rb
|
165
100
|
- spec/support/fakeredis.rb
|
166
|
-
has_rdoc:
|
data/.pairs
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
pairs:
|
2
|
-
ag: Atasay Gokkaya; atasay
|
3
|
-
km: Kaan Meralan; kaan
|
4
|
-
kg: Konstantin Gredeskoul; kig
|
5
|
-
ph: Paul Henry; paul
|
6
|
-
sf: Sean Flannagan; sean
|
7
|
-
es: Eric Saxby; sax
|
8
|
-
tn: Truong Nguyen; constantx
|
9
|
-
cc: Cihan Cimen; cihan
|
10
|
-
sc: Server Cimen; server
|
11
|
-
email:
|
12
|
-
prefix: pair
|
13
|
-
domain: wanelo.com
|