berater 0.1.4 → 0.6.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.
@@ -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