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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cebe59883ec6e4803896b89e61d88c53f43902cc
4
- data.tar.gz: 86067516015b359cbe2becb8d2e5b71748484f70
3
+ metadata.gz: b9d25d869302d10074000e45ae903a1d28d66ccc
4
+ data.tar.gz: d3bcfbce3b73826e20481aa28f6a1a70dc61f172
5
5
  SHA512:
6
- metadata.gz: 3538a2c610971934961be482f4531e43a1b614c4d48786484592f4e9c170ef5e4694055d1e8efb80839c62d636c93f997907fd5c9ed1df181adcf8bb6711d361
7
- data.tar.gz: bb3921bf9935e98176a073ac2dabcf43c1786c75ce5e532b04497dba4a8dff039d28ed9a1f88b12fd9a5da6052259e762e1cba1a66bd87bf7288f7d771021e42
6
+ metadata.gz: 812f85e93696fb48483f3d9096a9a6406509361a5145e3cd66614b57bb83e879ec4aa3fd749876a842966a2dd73a54344b0ea957d8e75ffd0bb0653c72aa9670
7
+ data.tar.gz: 29d51c794c8776748fa282ce63c308db83cc6092cbc6f0e407bb37b76140fa9fcc7bcd34847ecbc290f9e5f1b8e022b91e2f3ad17f700dfb5802c7aadf5e867c
data/.travis.yml CHANGED
@@ -5,6 +5,6 @@ script: "bundle exec rspec"
5
5
  notifications:
6
6
  email:
7
7
  recipients:
8
- - dev@wanelo.com
8
+ - dev-info@wanelo.com
9
9
  on_success: never
10
10
  on_failure: always
data/Gemfile CHANGED
@@ -5,3 +5,11 @@ source 'https://rubygems.org'
5
5
 
6
6
  # Specify your gem's dependencies in pause.gemspec
7
7
  gemspec
8
+
9
+ gem 'fakeredis'
10
+ gem 'guard-rspec'
11
+ gem 'pry-nav'
12
+ gem 'rake'
13
+ gem 'rb-fsevent'
14
+ gem 'rspec'
15
+ gem 'timecop'
data/Rakefile CHANGED
@@ -1 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/lib/pause.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  require 'redis'
2
- require "pause/version"
3
- require "pause/configuration"
4
- require "pause/action"
5
- require "pause/analyzer"
6
- require "pause/redis/adapter"
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 ||= Pause::Redis::Adapter.new(config)
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(key, timestamp, count)
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
- $stderr.puts "Error connecting to redis: #{e.inspect}"
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
  #
@@ -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.key)
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.key, period_check.block_ttl)
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)
@@ -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
@@ -0,0 +1,11 @@
1
+ module Pause
2
+ class Logger
3
+ def self.puts message
4
+ STDOUT.puts message
5
+ end
6
+
7
+ def self.fatal message
8
+ STDERR.puts message
9
+ end
10
+ end
11
+ end
@@ -15,8 +15,8 @@ module Pause
15
15
  @history = config.history
16
16
  end
17
17
 
18
- def increment(key, timestamp, count = 1)
19
- k = white_key(key)
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(key)
33
- extract_set_elements(white_key(key))
32
+ def key_history(scope, identifier)
33
+ extract_set_elements(tracked_key(scope, identifier))
34
34
  end
35
35
 
36
- def rate_limit!(key, block_ttl)
37
- redis.setex(rate_limited_key(key), block_ttl, nil)
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?(key)
41
- !!redis.get(rate_limited_key(key))
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(white_key(scope))
48
+ keys(tracked_scope(scope))
46
49
  end
47
50
 
48
51
  def rate_limited_keys(scope)
49
- keys(rate_limited_key(scope))
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
- delete_rate_limited_ids scope, rate_limited_keys(scope)
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
- delete_rate_limited_ids scope, [id]
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("disabled:#{scope}", "1")
68
+ redis.set("internal:|#{scope}|:disabled", "1")
62
69
  end
63
70
 
64
71
  def enable(scope)
65
- redis.del("disabled:#{scope}")
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.keys("disabled:#{scope}").first.nil?
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 delete_rate_limited_ids(scope, ids)
79
- increment_keys = ids.map{ |key| white_key(scope, key) }
80
- rate_limited_keys = ids.map{ |key| rate_limited_key(scope, key) }
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 white_key(scope, key = nil)
91
- ["i", scope, key].compact.join(':')
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 rate_limited_key(scope, key = nil)
95
- ["b", scope, key].compact.join(':')
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, :with_scores => true).map do |slice|
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
@@ -1,3 +1,3 @@
1
1
  module Pause
2
- VERSION = "0.0.6"
2
+ VERSION = "0.1.0"
3
3
  end
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
@@ -15,9 +15,10 @@ describe Pause::Action do
15
15
  let(:configuration) { Pause::Configuration.new }
16
16
 
17
17
  before do
18
- Pause.stub(:config).and_return(configuration)
19
- Pause.config.stub(:resolution).and_return(resolution)
20
- Pause.config.stub(:history).and_return(history)
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.should_receive(:increment).with(action.key, time.to_i, 1)
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?.should be_true
43
+ expect(action.ok?).to be true
43
44
  end
44
45
  action.increment!
45
- action.ok?.should be_false
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 = period_marker(resolution, Time.now.to_i)
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
- action.increment! 2, time - 3
56
- action.ok?.should be_true
53
+ Timecop.freeze time - 30 do
54
+ action.increment! 4
55
+ expect(action.ok?).to be true
56
+ end
57
57
 
58
- action.increment! 1, time
58
+ Timecop.freeze time do
59
+ action.increment! 2
60
+ expect(action.ok?).to be true
61
+ end
59
62
 
60
- action.ok?.should be_false
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
- Redis.any_instance.stub(:zrange) { raise Redis::CannotConnectError }
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?.should be_false
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.should be_nil
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.should be_a(Pause::RateLimitedEvent)
93
- rate_limit.identifier.should == expected_rate_limit.identifier
94
- rate_limit.sum.should == expected_rate_limit.sum
95
- rate_limit.period_check.should == expected_rate_limit.period_check
96
- rate_limit.timestamp.should == expected_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.should include(action.identifier)
110
- MyNotification.tracked_identifiers.should include(other_action.identifier)
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.should include(action.identifier)
123
- MyNotification.rate_limited_identifiers.should include(other_action.identifier)
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.should include(action.identifier, other_action.identifier)
136
- MyNotification.rate_limited_identifiers.should == [action.identifier]
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.should be_empty
141
- MyNotification.tracked_identifiers.should == [other_action.identifier]
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 be_false
156
+ expect(action.ok?).to be false
150
157
 
151
158
  action.unblock
152
159
 
153
- expect(action.ok?).to be_true
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.should == [
175
- Pause::PeriodCheck.new(100, 150, 200)
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.should == [
181
- Pause::PeriodCheck.new(100, 150, 200),
182
- Pause::PeriodCheck.new(200, 150, 200),
183
- Pause::PeriodCheck.new(300, 150, 200)
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.should == [
189
- Pause::PeriodCheck.new(50, 100, 60)
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
- lambda {
207
+ expect {
201
208
  UndefinedScopeAction.new("1.2.3.4").scope
202
- }.should raise_error("Should implement scope. (Ex: ipn:follow)")
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.should == "my: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.should be_enabled
232
- action.should_not be_disabled
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.should be_disabled
238
- action.should_not be_enabled
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.should_not be_enabled
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.should be_enabled
251
- action.should_not be_disabled
257
+ expect(action).to be_enabled
258
+ expect(action).to_not be_disabled
252
259
  end
253
260
  end
254
261
  end
@@ -15,9 +15,9 @@ describe Pause::Analyzer do
15
15
  let(:configuration) { Pause::Configuration.new }
16
16
 
17
17
  before do
18
- Pause.stub(:config).and_return(configuration)
19
- Pause.config.stub(:resolution).and_return(resolution)
20
- Pause.config.stub(:history).and_return(history)
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.should_receive(:rate_limit!).once.with(action.key, 12)
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).should be_nil
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).should be_a(Pause::RateLimitedEvent)
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.should == "128.23.12.8"
18
- subject.redis_port.should == 2134
19
- subject.redis_db.should == "13"
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.should == 5000
22
- subject.history.should == 6000
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.should == "127.0.0.1"
31
- subject.redis_port.should == 6379
32
- subject.redis_db.should == "1"
33
- subject.resolution.should == 600 # 10 minutes
34
- subject.history.should == 86400 # one day
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.stub(:config).and_return(configuration)
13
- Pause.config.stub(:resolution).and_return(resolution)
14
- Pause.config.stub(:history).and_return(history)
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(:key) { "213213" }
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(key, Time.now.to_i)
26
- set = redis_conn.zrange(adapter.send(:white_key, key), 0, -1, :with_scores => true)
27
- set.should_not be_empty
28
- set.size.should eql(1)
29
- set[0].size.should eql(2)
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.should_receive(:zrem).with(adapter.send(:white_key, key), [adapter.period_marker(resolution, time)])
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(key, Time.now.to_i)
40
+ adapter.increment(scope, identifier, Time.now.to_i)
39
41
  end
40
42
  Timecop.freeze time + (adapter.resolution + 1) do
41
- adapter.increment(key, Time.now.to_i)
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.should_receive(:expire).with(adapter.send(:white_key, key), history)
47
- adapter.increment(key, Time.now.to_i)
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 "#block" do
52
- let(:key) { "ipn:follow:123461234" }
53
- let(:blocked_key) { "b:#{key}" }
54
- let(:ttl) { 110000 }
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
- it "saves ip to redis with expiration" do
57
- adapter.rate_limit!(key, ttl)
58
- redis_conn.get(blocked_key).should_not be_nil
59
- redis_conn.ttl(blocked_key).should == ttl
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 "#blocked?" do
64
- let(:key) { "ipn:follow:123461234" }
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!(key, ttl)
70
- (!!redis_conn.get(blocked_key).should) == adapter.rate_limited?(key)
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 "#white_key" do
91
+ describe "#tracked_key" do
75
92
  it "prefixes key" do
76
- adapter.send(:white_key, "abc").should == "i:abc"
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 be_true
100
+ expect(adapter.disabled?("boom")).to be true
84
101
  adapter.enable("boom")
85
- expect(adapter.disabled?("boom")).to be_false
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 be_true
108
+ expect(adapter.enabled?("boom")).to be true
92
109
  adapter.disable("boom")
93
- expect(adapter.enabled?("boom")).to be_false
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 be_false
100
- adapter.rate_limit!('1', 10)
101
- expect(adapter.rate_limited?('1')).to be_true
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:1', 10)
108
- adapter.rate_limit!('boom:2', 10)
141
+ adapter.rate_limit!('boom', '1', 10)
142
+ adapter.rate_limit!('boom', '2', 10)
109
143
 
110
- expect(adapter.rate_limited?('boom:1')).to be_true
111
- expect(adapter.rate_limited?('boom:2')).to be_true
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:1')).to be_false
116
- expect(adapter.rate_limited?('boom:2')).to be_false
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:1', 10)
123
- adapter.rate_limit!('boom:2', 10)
156
+ adapter.rate_limit!('boom', '1', 10)
157
+ adapter.rate_limit!('boom', '2', 10)
124
158
 
125
- expect(adapter.rate_limited?('boom:1')).to be_true
126
- expect(adapter.rate_limited?('boom:2')).to be_true
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:1')).to be_false
131
- expect(adapter.rate_limited?('boom:2')).to be_true
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
 
@@ -1,3 +1,2 @@
1
- require 'fakeredis'
2
1
  require 'fakeredis/rspec'
3
2
 
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.6
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: 2014-05-23 00:00:00.000000000 Z
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.0
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