pause 0.0.6 → 0.1.0
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/.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
|