berater 0.1.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.1.4'
2
+ VERSION = '0.6.0'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -7,12 +7,12 @@ describe Berater do
7
7
  it { is_expected.to respond_to :configure }
8
8
 
9
9
  describe '.configure' do
10
- it 'can be set via configure' do
10
+ it 'is used with a block' do
11
11
  Berater.configure do |c|
12
- c.mode = :rate
12
+ c.redis = :redis
13
13
  end
14
14
 
15
- expect(Berater.mode).to eq :rate
15
+ expect(Berater.redis).to be :redis
16
16
  end
17
17
  end
18
18
 
@@ -24,20 +24,13 @@ describe Berater do
24
24
  end
25
25
  end
26
26
 
27
- describe '.mode' do
28
- it 'validates inputs' do
29
- expect { Berater.mode = :foo }.to raise_error(ArgumentError)
30
- end
31
- end
32
-
33
- context 'unlimited mode' do
34
- before { Berater.mode = :unlimited }
35
-
36
- describe '.new' do
37
- let(:limiter) { Berater.new(:unlimited) }
27
+ describe '.new' do
28
+ context 'unlimited mode' do
29
+ let(:limiter) { Berater.new(:key, Float::INFINITY) }
38
30
 
39
31
  it 'instantiates an Unlimiter' do
40
32
  expect(limiter).to be_a Berater::Unlimiter
33
+ expect(limiter.key).to be :key
41
34
  end
42
35
 
43
36
  it 'inherits redis' do
@@ -46,35 +39,17 @@ describe Berater do
46
39
 
47
40
  it 'accepts options' do
48
41
  redis = double('Redis')
49
- limiter = Berater.new(:unlimited, key: 'key', redis: redis)
50
- expect(limiter.key).to match(/key/)
42
+ limiter = Berater.new(:key, Float::INFINITY, redis: redis)
51
43
  expect(limiter.redis).to be redis
52
44
  end
53
45
  end
54
46
 
55
- describe '.limit' do
56
- it 'works' do
57
- expect(Berater.limit).to be_nil
58
- end
59
-
60
- it 'yields' do
61
- expect {|b| Berater.limit(&b) }.to yield_control
62
- end
63
-
64
- it 'never limits' do
65
- 10.times { expect(Berater.limit { 123 } ).to eq 123 }
66
- end
67
- end
68
- end
69
-
70
- context 'inhibited mode' do
71
- before { Berater.mode = :inhibited }
72
-
73
- describe '.new' do
74
- let(:limiter) { Berater.new(:inhibited) }
47
+ context 'inhibited mode' do
48
+ let(:limiter) { Berater.new(:key, 0) }
75
49
 
76
50
  it 'instantiates an Inhibitor' do
77
51
  expect(limiter).to be_a Berater::Inhibitor
52
+ expect(limiter.key).to be :key
78
53
  end
79
54
 
80
55
  it 'inherits redis' do
@@ -83,27 +58,17 @@ describe Berater do
83
58
 
84
59
  it 'accepts options' do
85
60
  redis = double('Redis')
86
- limiter = Berater.new(:inhibited, key: 'key', redis: redis)
87
- expect(limiter.key).to match(/key/)
61
+ limiter = Berater.new(:key, 0, redis: redis)
88
62
  expect(limiter.redis).to be redis
89
63
  end
90
64
  end
91
65
 
92
- describe '.limit' do
93
- it 'always limits' do
94
- expect { Berater.limit }.to be_inhibited
95
- end
96
- end
97
- end
98
-
99
- context 'rate mode' do
100
- before { Berater.mode = :rate }
101
-
102
- describe '.limiter' do
103
- let(:limiter) { Berater.new(:rate, 1, :second) }
66
+ context 'rate mode' do
67
+ let(:limiter) { Berater.new(:key, 1, :second) }
104
68
 
105
69
  it 'instantiates a RateLimiter' do
106
70
  expect(limiter).to be_a Berater::RateLimiter
71
+ expect(limiter.key).to be :key
107
72
  end
108
73
 
109
74
  it 'inherits redis' do
@@ -112,44 +77,17 @@ describe Berater do
112
77
 
113
78
  it 'accepts options' do
114
79
  redis = double('Redis')
115
- limiter = Berater.new(:rate, 1, :second, key: 'key', redis: redis)
116
- expect(limiter.key).to match(/key/)
80
+ limiter = Berater.new(:key, 1, :second, redis: redis)
117
81
  expect(limiter.redis).to be redis
118
82
  end
119
83
  end
120
84
 
121
- describe '.limit' do
122
- it 'works' do
123
- expect(Berater.limit(1, :second)).to eq 1
124
- end
125
-
126
- it 'yields' do
127
- expect {|b| Berater.limit(2, :second, &b) }.to yield_control
128
- expect(Berater.limit(2, :second) { 123 }).to eq 123
129
- end
130
-
131
- it 'limits excessive calls' do
132
- expect(Berater.limit(1, :second)).to eq 1
133
- expect { Berater.limit(1, :second) }.to be_overrated
134
- end
135
-
136
- it 'accepts options' do
137
- redis = double('Redis')
138
- expect(redis).to receive(:multi)
139
-
140
- Berater.limit(1, :second, redis: redis) rescue nil
141
- end
142
- end
143
- end
144
-
145
- context 'concurrency mode' do
146
- before { Berater.mode = :concurrency }
147
-
148
- describe '.limiter' do
149
- let(:limiter) { Berater.new(:concurrency, 1) }
85
+ context 'concurrency mode' do
86
+ let(:limiter) { Berater.new(:key, 1) }
150
87
 
151
88
  it 'instantiates a ConcurrencyLimiter' do
152
89
  expect(limiter).to be_a Berater::ConcurrencyLimiter
90
+ expect(limiter.key).to be :key
153
91
  end
154
92
 
155
93
  it 'inherits redis' do
@@ -158,35 +96,39 @@ describe Berater do
158
96
 
159
97
  it 'accepts options' do
160
98
  redis = double('Redis')
161
- limiter = Berater.new(:concurrency, 1, key: 'key', redis: redis)
162
- expect(limiter.key).to match(/key/)
99
+ limiter = Berater.new(:key, 1, redis: redis)
163
100
  expect(limiter.redis).to be redis
164
101
  end
165
102
  end
103
+ end
166
104
 
167
- describe '.limit' do
168
- it 'works (without blocks by returning a lock)' do
169
- lock = Berater.limit(1)
170
- expect(lock).to be_a Berater::ConcurrencyLimiter::Lock
171
- expect(lock.release).to be true
172
- end
173
-
174
- it 'yields' do
175
- expect {|b| Berater.limit(1, &b) }.to yield_control
176
- end
177
-
178
- it 'limits excessive calls' do
179
- Berater.limit(1)
180
- expect { Berater.limit(1) }.to be_incapacitated
105
+ describe 'Berater() - convenience method' do
106
+ RSpec.shared_examples 'test convenience' do |klass, *args|
107
+ it 'creates a limiter' do
108
+ limiter = Berater(:key, *args)
109
+ expect(limiter).to be_a klass
181
110
  end
182
111
 
183
- it 'accepts options' do
184
- redis = double('Redis')
185
- expect(redis).to receive(:eval)
112
+ context 'with a block' do
113
+ it 'creates a limiter and calls limit' do
114
+ limiter = Berater(:key, *args)
115
+ expect(klass).to receive(:new).and_return(limiter)
116
+ expect(limiter).to receive(:limit).and_call_original
186
117
 
187
- Berater.limit(1, redis: redis) rescue nil
118
+ begin
119
+ res = Berater(:key, *args) { true }
120
+ expect(res).to be true
121
+ rescue Berater::Overloaded
122
+ expect(klass).to be Berater::Inhibitor
123
+ end
124
+ end
188
125
  end
189
126
  end
127
+
128
+ include_examples 'test convenience', Berater::Unlimiter, Float::INFINITY
129
+ include_examples 'test convenience', Berater::Inhibitor, 0
130
+ include_examples 'test convenience', Berater::RateLimiter, 1, :second
131
+ include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
190
132
  end
191
133
 
192
134
  end
@@ -1,37 +1,38 @@
1
1
  describe Berater::ConcurrencyLimiter do
2
- before { Berater.mode = :concurrency }
2
+ it_behaves_like 'a limiter', described_class.new(:key, 1)
3
+ it_behaves_like 'a limiter', described_class.new(:key, 1, timeout: 1)
3
4
 
4
5
  describe '.new' do
5
- let(:limiter) { described_class.new(1) }
6
+ let(:limiter) { described_class.new(:key, 1) }
6
7
 
7
8
  it 'initializes' do
9
+ expect(limiter.key).to be :key
8
10
  expect(limiter.capacity).to be 1
9
11
  end
10
12
 
11
13
  it 'has default values' do
12
- expect(limiter.key).to eq described_class.to_s
13
14
  expect(limiter.redis).to be Berater.redis
14
15
  end
15
16
  end
16
17
 
17
18
  describe '#capacity' do
18
19
  def expect_capacity(capacity)
19
- limiter = described_class.new(capacity)
20
+ limiter = described_class.new(:key, capacity)
20
21
  expect(limiter.capacity).to eq capacity
21
22
  end
22
23
 
23
24
  it { expect_capacity(0) }
24
25
  it { expect_capacity(1) }
26
+ it { expect_capacity(1.5) }
25
27
  it { expect_capacity(10_000) }
26
28
 
27
29
  context 'with erroneous values' do
28
30
  def expect_bad_capacity(capacity)
29
31
  expect do
30
- described_class.new(capacity)
32
+ described_class.new(:key, capacity)
31
33
  end.to raise_error ArgumentError
32
34
  end
33
35
 
34
- it { expect_bad_capacity(0.5) }
35
36
  it { expect_bad_capacity(-1) }
36
37
  it { expect_bad_capacity('1') }
37
38
  it { expect_bad_capacity(:one) }
@@ -39,31 +40,23 @@ describe Berater::ConcurrencyLimiter do
39
40
  end
40
41
 
41
42
  describe '#timeout' do
42
- def expect_timeout(timeout)
43
- limiter = described_class.new(1, timeout: timeout)
44
- expect(limiter.timeout).to eq timeout
45
- end
46
-
47
- it { expect_timeout(0) }
48
- it { expect_timeout(1) }
49
- it { expect_timeout(10_000) }
43
+ # see spec/utils_spec.rb
50
44
 
51
- context 'with erroneous values' do
52
- def expect_bad_timeout(timeout)
53
- expect do
54
- described_class.new(1, timeout: timeout)
55
- end.to raise_error ArgumentError
56
- end
45
+ it 'saves the interval in original and millisecond format' do
46
+ limiter = described_class.new(:key, 1, timeout: 3)
47
+ expect(limiter.timeout).to be 3
48
+ expect(limiter.instance_variable_get(:@timeout_msec)).to be (3 * 10**3)
49
+ end
57
50
 
58
- it { expect_bad_timeout(0.5) }
59
- it { expect_bad_timeout(-1) }
60
- it { expect_bad_timeout('1') }
61
- it { expect_bad_timeout(:one) }
51
+ it 'handles infinity' do
52
+ limiter = described_class.new(:key, 1, timeout: Float::INFINITY)
53
+ expect(limiter.timeout).to be Float::INFINITY
54
+ expect(limiter.instance_variable_get(:@timeout_msec)).to be 0
62
55
  end
63
56
  end
64
57
 
65
58
  describe '#limit' do
66
- let(:limiter) { described_class.new(2, timeout: 1) }
59
+ let(:limiter) { described_class.new(:key, 2, timeout: 30) }
67
60
 
68
61
  it 'works' do
69
62
  expect {|b| limiter.limit(&b) }.to yield_control
@@ -81,121 +74,196 @@ describe Berater::ConcurrencyLimiter do
81
74
  end
82
75
 
83
76
  it 'limits excessive calls' do
84
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
85
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
77
+ expect(limiter.limit).to be_a Berater::Lock
78
+ expect(limiter.limit).to be_a Berater::Lock
79
+
80
+ expect(limiter).to be_incapacitated
81
+ end
82
+
83
+ context 'with capacity 0' do
84
+ let(:limiter) { described_class.new(:key, 0) }
85
+
86
+ it 'always fails' do
87
+ expect(limiter).to be_incapacitated
88
+ end
89
+ end
90
+
91
+ context 'when capacity is a Float' do
92
+ let(:limiter) { described_class.new(:key, 1.5) }
93
+
94
+ it 'still works' do
95
+ lock = limiter.limit
96
+
97
+ # since fractional cost is not supported
98
+ expect(lock.capacity).to be 1
99
+ expect(limiter).to be_incapacitated
100
+ end
101
+ end
86
102
 
103
+ it 'limit resets over time' do
104
+ 2.times { limiter.limit }
105
+ expect(limiter).to be_incapacitated
106
+
107
+ Timecop.freeze(30)
108
+
109
+ 2.times { limiter.limit }
87
110
  expect(limiter).to be_incapacitated
88
111
  end
89
112
 
90
- it 'times out locks' do
91
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
92
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
113
+ it 'limit resets with millisecond precision' do
114
+ 2.times { limiter.limit }
93
115
  expect(limiter).to be_incapacitated
94
116
 
95
- Timecop.travel(1)
117
+ # travel forward to just before first lock times out
118
+ Timecop.freeze(29.999)
119
+ expect(limiter).to be_incapacitated
96
120
 
97
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
98
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
121
+ # traveling one more millisecond will decrement the count
122
+ Timecop.freeze(0.001)
123
+ 2.times { limiter.limit }
99
124
  expect(limiter).to be_incapacitated
100
125
  end
101
126
 
102
- context 'with capacity 0' do
103
- let(:limiter) { described_class.new(0) }
127
+ context 'with cost parameter' do
128
+ it { expect { limiter.limit(cost: 4) }.to be_incapacitated }
104
129
 
105
- it 'always fails' do
130
+ it 'works within limit' do
131
+ limiter.limit(cost: 2)
106
132
  expect(limiter).to be_incapacitated
107
133
  end
108
- end
109
- end
110
134
 
111
- context 'with same key, different limiters' do
112
- let(:limiter_one) { described_class.new(1) }
113
- let(:limiter_two) { described_class.new(1) }
135
+ it 'releases full cost' do
136
+ lock = limiter.limit(cost: 2)
137
+ expect(limiter).to be_incapacitated
138
+
139
+ lock.release
140
+ expect(limiter).not_to be_incapacitated
114
141
 
115
- it { expect(limiter_one.key).to eq limiter_two.key }
142
+ lock = limiter.limit(cost: 2)
143
+ expect(limiter).to be_incapacitated
144
+ end
116
145
 
117
- it 'works as expected' do
118
- expect(limiter_one.limit).to be_a Berater::ConcurrencyLimiter::Lock
146
+ it 'respects timeout' do
147
+ limiter.limit(cost: 2)
148
+ expect(limiter).to be_incapacitated
119
149
 
120
- expect(limiter_one).to be_incapacitated
121
- expect(limiter_two).to be_incapacitated
122
- end
123
- end
150
+ Timecop.freeze(30)
151
+ expect(limiter).not_to be_incapacitated
124
152
 
125
- context 'with different keys, same limiter' do
126
- let(:limiter) { described_class.new(1) }
153
+ limiter.limit(cost: 2)
154
+ expect(limiter).to be_incapacitated
155
+ end
127
156
 
128
- it 'works as expected' do
129
- one_lock = limiter.limit(key: :one)
130
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
157
+ it 'accepts a dynamic capacity' do
158
+ limiter = described_class.new(:key, 1)
159
+
160
+ expect { limiter.limit(capacity: 0) }.to be_incapacitated
161
+ 5.times { limiter.limit(capacity: 10) }
162
+ expect { limiter }.to be_incapacitated
163
+ end
131
164
 
132
- expect { limiter.limit(key: :one) {} }.to be_incapacitated
133
- expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
165
+ it 'only allows positive, Integer values' do
166
+ expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
167
+ expect { limiter.limit(cost: 1.5) }.to raise_error(ArgumentError)
168
+ end
169
+ end
134
170
 
135
- two_lock = limiter.limit(key: :two)
136
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
171
+ context 'with same key, different limiters' do
172
+ let(:limiter_one) { described_class.new(:key, 1) }
173
+ let(:limiter_two) { described_class.new(:key, 1) }
137
174
 
138
- expect { limiter.limit(key: :one) {} }.to be_incapacitated
139
- expect { limiter.limit(key: :two) {} }.to be_incapacitated
175
+ it { expect(limiter_one.key).to eq limiter_two.key }
140
176
 
141
- one_lock.release
142
- expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
143
- expect { limiter.limit(key: :two) {} }.to be_incapacitated
177
+ it 'works as expected' do
178
+ expect(limiter_one.limit).to be_a Berater::Lock
144
179
 
145
- two_lock.release
146
- expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
147
- expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
180
+ expect(limiter_one).to be_incapacitated
181
+ expect(limiter_two).to be_incapacitated
182
+ end
148
183
  end
149
- end
150
184
 
151
- context 'with same key, different capacities' do
152
- let(:limiter_one) { described_class.new(1) }
153
- let(:limiter_two) { described_class.new(2) }
185
+ context 'with same key, different capacities' do
186
+ let(:limiter_one) { described_class.new(:key, 1) }
187
+ let(:limiter_two) { described_class.new(:key, 2) }
154
188
 
155
- it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
189
+ it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
156
190
 
157
- it 'works as expected' do
158
- one_lock = limiter_one.limit
159
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
191
+ it 'works as expected' do
192
+ one_lock = limiter_one.limit
193
+ expect(one_lock).to be_a Berater::Lock
160
194
 
161
- expect(limiter_one).to be_incapacitated
162
- expect(limiter_two).not_to be_incapacitated
195
+ expect(limiter_one).to be_incapacitated
196
+ expect(limiter_two).not_to be_incapacitated
163
197
 
164
- two_lock = limiter_two.limit
165
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
198
+ two_lock = limiter_two.limit
199
+ expect(two_lock).to be_a Berater::Lock
166
200
 
167
- expect(limiter_one).to be_incapacitated
168
- expect(limiter_two).to be_incapacitated
201
+ expect(limiter_one).to be_incapacitated
202
+ expect(limiter_two).to be_incapacitated
169
203
 
170
- one_lock.release
171
- expect(limiter_one).to be_incapacitated
172
- expect(limiter_two).not_to be_incapacitated
204
+ one_lock.release
205
+ expect(limiter_one).to be_incapacitated
206
+ expect(limiter_two).not_to be_incapacitated
173
207
 
174
- two_lock.release
175
- expect(limiter_one).not_to be_incapacitated
176
- expect(limiter_two).not_to be_incapacitated
208
+ two_lock.release
209
+ expect(limiter_one).not_to be_incapacitated
210
+ expect(limiter_two).not_to be_incapacitated
211
+ end
177
212
  end
178
- end
179
213
 
180
- context 'with different keys, different limiters' do
181
- let(:limiter_one) { described_class.new(1, key: :one) }
182
- let(:limiter_two) { described_class.new(1, key: :two) }
214
+ context 'with different keys, different limiters' do
215
+ let(:limiter_one) { described_class.new(:one, 1) }
216
+ let(:limiter_two) { described_class.new(:two, 1) }
217
+
218
+ it 'works as expected' do
219
+ expect(limiter_one).not_to be_incapacitated
220
+ expect(limiter_two).not_to be_incapacitated
221
+
222
+ one_lock = limiter_one.limit
223
+ expect(one_lock).to be_a Berater::Lock
224
+
225
+ expect(limiter_one).to be_incapacitated
226
+ expect(limiter_two).not_to be_incapacitated
227
+
228
+ two_lock = limiter_two.limit
229
+ expect(two_lock).to be_a Berater::Lock
230
+
231
+ expect(limiter_one).to be_incapacitated
232
+ expect(limiter_two).to be_incapacitated
233
+ end
234
+ end
235
+ end
183
236
 
184
- it 'works as expected' do
185
- expect(limiter_one).not_to be_incapacitated
186
- expect(limiter_two).not_to be_incapacitated
237
+ describe '#overloaded?' do
238
+ let(:limiter) { described_class.new(:key, 1, timeout: 30) }
187
239
 
188
- one_lock = limiter_one.limit
189
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
240
+ it 'works' do
241
+ expect(limiter.overloaded?).to be false
242
+ lock = limiter.limit
243
+ expect(limiter.overloaded?).to be true
244
+ lock.release
245
+ expect(limiter.overloaded?).to be false
246
+ end
190
247
 
191
- expect(limiter_one).to be_incapacitated
192
- expect(limiter_two).not_to be_incapacitated
248
+ it 'respects timeout' do
249
+ expect(limiter.overloaded?).to be false
250
+ lock = limiter.limit
251
+ expect(limiter.overloaded?).to be true
252
+ Timecop.freeze(30)
253
+ expect(limiter.overloaded?).to be false
254
+ end
255
+ end
193
256
 
194
- two_lock = limiter_two.limit
195
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
257
+ describe '#to_s' do
258
+ def check(capacity, expected)
259
+ expect(
260
+ described_class.new(:key, capacity).to_s
261
+ ).to match(expected)
262
+ end
196
263
 
197
- expect(limiter_one).to be_incapacitated
198
- expect(limiter_two).to be_incapacitated
264
+ it 'works' do
265
+ check(1, /1 at a time/)
266
+ check(3, /3 at a time/)
199
267
  end
200
268
  end
201
269