berater 0.6.0 → 0.8.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/lib/berater.rb +14 -13
- data/lib/berater/concurrency_limiter.rb +19 -37
- data/lib/berater/dsl.rb +8 -8
- data/lib/berater/inhibitor.rb +4 -7
- data/lib/berater/limiter.rb +49 -19
- data/lib/berater/lock.rb +2 -1
- data/lib/berater/rate_limiter.rb +40 -40
- data/lib/berater/rspec.rb +1 -2
- data/lib/berater/rspec/matchers.rb +26 -48
- data/lib/berater/static_limiter.rb +49 -0
- data/lib/berater/test_mode.rb +31 -36
- data/lib/berater/unlimiter.rb +8 -6
- data/lib/berater/utils.rb +9 -0
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +26 -76
- data/spec/concurrency_limiter_spec.rb +73 -57
- data/spec/dsl_refinement_spec.rb +0 -12
- data/spec/dsl_spec.rb +5 -17
- data/spec/inhibitor_spec.rb +10 -5
- data/spec/limiter_spec.rb +95 -9
- data/spec/matchers_spec.rb +21 -85
- data/spec/rate_limiter_spec.rb +88 -38
- data/spec/riddle_spec.rb +6 -2
- data/spec/static_limiter_spec.rb +79 -0
- data/spec/test_mode_spec.rb +48 -106
- data/spec/unlimiter_spec.rb +11 -5
- metadata +5 -2
@@ -18,7 +18,7 @@ describe Berater::ConcurrencyLimiter do
|
|
18
18
|
describe '#capacity' do
|
19
19
|
def expect_capacity(capacity)
|
20
20
|
limiter = described_class.new(:key, capacity)
|
21
|
-
expect(limiter.capacity).to eq capacity
|
21
|
+
expect(limiter.capacity).to eq capacity.to_i
|
22
22
|
end
|
23
23
|
|
24
24
|
it { expect_capacity(0) }
|
@@ -36,22 +36,28 @@ describe Berater::ConcurrencyLimiter do
|
|
36
36
|
it { expect_bad_capacity(-1) }
|
37
37
|
it { expect_bad_capacity('1') }
|
38
38
|
it { expect_bad_capacity(:one) }
|
39
|
+
it { expect_bad_capacity(Float::INFINITY) }
|
39
40
|
end
|
40
41
|
end
|
41
42
|
|
42
43
|
describe '#timeout' do
|
43
44
|
# see spec/utils_spec.rb
|
44
45
|
|
46
|
+
it 'defaults to nil' do
|
47
|
+
limiter = described_class.new(:key, 1)
|
48
|
+
expect(limiter.timeout).to be nil
|
49
|
+
end
|
50
|
+
|
45
51
|
it 'saves the interval in original and millisecond format' do
|
46
52
|
limiter = described_class.new(:key, 1, timeout: 3)
|
47
53
|
expect(limiter.timeout).to be 3
|
48
|
-
expect(limiter.instance_variable_get(:@
|
54
|
+
expect(limiter.instance_variable_get(:@timeout)).to be (3 * 10**3)
|
49
55
|
end
|
50
56
|
|
51
57
|
it 'handles infinity' do
|
52
58
|
limiter = described_class.new(:key, 1, timeout: Float::INFINITY)
|
53
59
|
expect(limiter.timeout).to be Float::INFINITY
|
54
|
-
expect(limiter.instance_variable_get(:@
|
60
|
+
expect(limiter.instance_variable_get(:@timeout)).to be 0
|
55
61
|
end
|
56
62
|
end
|
57
63
|
|
@@ -77,14 +83,14 @@ describe Berater::ConcurrencyLimiter do
|
|
77
83
|
expect(limiter.limit).to be_a Berater::Lock
|
78
84
|
expect(limiter.limit).to be_a Berater::Lock
|
79
85
|
|
80
|
-
expect(limiter).to
|
86
|
+
expect(limiter).to be_overloaded
|
81
87
|
end
|
82
88
|
|
83
89
|
context 'with capacity 0' do
|
84
90
|
let(:limiter) { described_class.new(:key, 0) }
|
85
91
|
|
86
92
|
it 'always fails' do
|
87
|
-
expect(limiter).to
|
93
|
+
expect(limiter).to be_overloaded
|
88
94
|
end
|
89
95
|
end
|
90
96
|
|
@@ -96,75 +102,85 @@ describe Berater::ConcurrencyLimiter do
|
|
96
102
|
|
97
103
|
# since fractional cost is not supported
|
98
104
|
expect(lock.capacity).to be 1
|
99
|
-
expect(limiter).to
|
105
|
+
expect(limiter).to be_overloaded
|
100
106
|
end
|
101
107
|
end
|
102
108
|
|
103
109
|
it 'limit resets over time' do
|
104
110
|
2.times { limiter.limit }
|
105
|
-
expect(limiter).to
|
111
|
+
expect(limiter).to be_overloaded
|
106
112
|
|
107
113
|
Timecop.freeze(30)
|
108
114
|
|
109
115
|
2.times { limiter.limit }
|
110
|
-
expect(limiter).to
|
116
|
+
expect(limiter).to be_overloaded
|
111
117
|
end
|
112
118
|
|
113
119
|
it 'limit resets with millisecond precision' do
|
114
120
|
2.times { limiter.limit }
|
115
|
-
expect(limiter).to
|
121
|
+
expect(limiter).to be_overloaded
|
116
122
|
|
117
123
|
# travel forward to just before first lock times out
|
118
124
|
Timecop.freeze(29.999)
|
119
|
-
expect(limiter).to
|
125
|
+
expect(limiter).to be_overloaded
|
120
126
|
|
121
127
|
# traveling one more millisecond will decrement the count
|
122
128
|
Timecop.freeze(0.001)
|
123
129
|
2.times { limiter.limit }
|
124
|
-
expect(limiter).to
|
130
|
+
expect(limiter).to be_overloaded
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'accepts a dynamic capacity' do
|
134
|
+
expect { limiter.limit(capacity: 0) }.to be_overloaded
|
135
|
+
5.times { limiter.limit(capacity: 10) }
|
136
|
+
expect { limiter }.to be_overloaded
|
125
137
|
end
|
126
138
|
|
127
139
|
context 'with cost parameter' do
|
128
|
-
it { expect { limiter.limit(cost: 4) }.to
|
140
|
+
it { expect { limiter.limit(cost: 4) }.to be_overloaded }
|
129
141
|
|
130
142
|
it 'works within limit' do
|
131
143
|
limiter.limit(cost: 2)
|
132
|
-
expect(limiter).to
|
144
|
+
expect(limiter).to be_overloaded
|
133
145
|
end
|
134
146
|
|
135
147
|
it 'releases full cost' do
|
136
148
|
lock = limiter.limit(cost: 2)
|
137
|
-
expect(limiter).to
|
149
|
+
expect(limiter).to be_overloaded
|
138
150
|
|
139
151
|
lock.release
|
140
|
-
expect(limiter).not_to
|
152
|
+
expect(limiter).not_to be_overloaded
|
141
153
|
|
142
154
|
lock = limiter.limit(cost: 2)
|
143
|
-
expect(limiter).to
|
155
|
+
expect(limiter).to be_overloaded
|
144
156
|
end
|
145
157
|
|
146
158
|
it 'respects timeout' do
|
147
159
|
limiter.limit(cost: 2)
|
148
|
-
expect(limiter).to
|
160
|
+
expect(limiter).to be_overloaded
|
149
161
|
|
150
162
|
Timecop.freeze(30)
|
151
|
-
expect(limiter).not_to
|
163
|
+
expect(limiter).not_to be_overloaded
|
152
164
|
|
153
165
|
limiter.limit(cost: 2)
|
154
|
-
expect(limiter).to
|
166
|
+
expect(limiter).to be_overloaded
|
155
167
|
end
|
156
168
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
169
|
+
context 'with fractional costs' do
|
170
|
+
it 'rounds up' do
|
171
|
+
limiter.limit(cost: 1.5)
|
172
|
+
expect(limiter).to be_overloaded
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'accumulates correctly' do
|
176
|
+
limiter.limit(cost: 0.5) # => 1
|
177
|
+
limiter.limit(cost: 0.7) # => 2
|
178
|
+
expect(limiter).to be_overloaded
|
179
|
+
end
|
163
180
|
end
|
164
181
|
|
165
|
-
it 'only allows positive
|
182
|
+
it 'only allows positive values' do
|
166
183
|
expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
|
167
|
-
expect { limiter.limit(cost: 1.5) }.to raise_error(ArgumentError)
|
168
184
|
end
|
169
185
|
end
|
170
186
|
|
@@ -177,8 +193,8 @@ describe Berater::ConcurrencyLimiter do
|
|
177
193
|
it 'works as expected' do
|
178
194
|
expect(limiter_one.limit).to be_a Berater::Lock
|
179
195
|
|
180
|
-
expect(limiter_one).to
|
181
|
-
expect(limiter_two).to
|
196
|
+
expect(limiter_one).to be_overloaded
|
197
|
+
expect(limiter_two).to be_overloaded
|
182
198
|
end
|
183
199
|
end
|
184
200
|
|
@@ -192,22 +208,22 @@ describe Berater::ConcurrencyLimiter do
|
|
192
208
|
one_lock = limiter_one.limit
|
193
209
|
expect(one_lock).to be_a Berater::Lock
|
194
210
|
|
195
|
-
expect(limiter_one).to
|
196
|
-
expect(limiter_two).not_to
|
211
|
+
expect(limiter_one).to be_overloaded
|
212
|
+
expect(limiter_two).not_to be_overloaded
|
197
213
|
|
198
214
|
two_lock = limiter_two.limit
|
199
215
|
expect(two_lock).to be_a Berater::Lock
|
200
216
|
|
201
|
-
expect(limiter_one).to
|
202
|
-
expect(limiter_two).to
|
217
|
+
expect(limiter_one).to be_overloaded
|
218
|
+
expect(limiter_two).to be_overloaded
|
203
219
|
|
204
220
|
one_lock.release
|
205
|
-
expect(limiter_one).to
|
206
|
-
expect(limiter_two).not_to
|
221
|
+
expect(limiter_one).to be_overloaded
|
222
|
+
expect(limiter_two).not_to be_overloaded
|
207
223
|
|
208
224
|
two_lock.release
|
209
|
-
expect(limiter_one).not_to
|
210
|
-
expect(limiter_two).not_to
|
225
|
+
expect(limiter_one).not_to be_overloaded
|
226
|
+
expect(limiter_two).not_to be_overloaded
|
211
227
|
end
|
212
228
|
end
|
213
229
|
|
@@ -216,41 +232,41 @@ describe Berater::ConcurrencyLimiter do
|
|
216
232
|
let(:limiter_two) { described_class.new(:two, 1) }
|
217
233
|
|
218
234
|
it 'works as expected' do
|
219
|
-
expect(limiter_one).not_to
|
220
|
-
expect(limiter_two).not_to
|
235
|
+
expect(limiter_one).not_to be_overloaded
|
236
|
+
expect(limiter_two).not_to be_overloaded
|
221
237
|
|
222
238
|
one_lock = limiter_one.limit
|
223
239
|
expect(one_lock).to be_a Berater::Lock
|
224
240
|
|
225
|
-
expect(limiter_one).to
|
226
|
-
expect(limiter_two).not_to
|
241
|
+
expect(limiter_one).to be_overloaded
|
242
|
+
expect(limiter_two).not_to be_overloaded
|
227
243
|
|
228
244
|
two_lock = limiter_two.limit
|
229
245
|
expect(two_lock).to be_a Berater::Lock
|
230
246
|
|
231
|
-
expect(limiter_one).to
|
232
|
-
expect(limiter_two).to
|
247
|
+
expect(limiter_one).to be_overloaded
|
248
|
+
expect(limiter_two).to be_overloaded
|
233
249
|
end
|
234
250
|
end
|
235
251
|
end
|
236
252
|
|
237
|
-
describe '#
|
238
|
-
let(:limiter) { described_class.new(:key,
|
253
|
+
describe '#utilization' do
|
254
|
+
let(:limiter) { described_class.new(:key, 10, timeout: 30) }
|
239
255
|
|
240
256
|
it 'works' do
|
241
|
-
expect(limiter.
|
242
|
-
lock = limiter.limit
|
243
|
-
expect(limiter.overloaded?).to be true
|
244
|
-
lock.release
|
245
|
-
expect(limiter.overloaded?).to be false
|
246
|
-
end
|
257
|
+
expect(limiter.utilization).to be 0.0
|
247
258
|
|
248
|
-
|
249
|
-
expect(limiter.
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
259
|
+
2.times { limiter.limit }
|
260
|
+
expect(limiter.utilization).to be 0.2
|
261
|
+
|
262
|
+
Timecop.freeze(15)
|
263
|
+
|
264
|
+
8.times { limiter.limit }
|
265
|
+
expect(limiter.utilization).to be 1.0
|
266
|
+
|
267
|
+
Timecop.freeze(15)
|
268
|
+
|
269
|
+
expect(limiter.utilization).to be 0.8
|
254
270
|
end
|
255
271
|
end
|
256
272
|
|
data/spec/dsl_refinement_spec.rb
CHANGED
@@ -3,18 +3,6 @@ require 'berater/dsl'
|
|
3
3
|
describe Berater do
|
4
4
|
using Berater::DSL
|
5
5
|
|
6
|
-
it 'instatiates an Unlimiter' do
|
7
|
-
limiter = Berater.new(:key) { unlimited }
|
8
|
-
expect(limiter).to be_a Berater::Unlimiter
|
9
|
-
expect(limiter.key).to be :key
|
10
|
-
end
|
11
|
-
|
12
|
-
it 'instatiates an Inhibiter' do
|
13
|
-
limiter = Berater.new(:key) { inhibited }
|
14
|
-
expect(limiter).to be_a Berater::Inhibitor
|
15
|
-
expect(limiter.key).to be :key
|
16
|
-
end
|
17
|
-
|
18
6
|
it 'instatiates a RateLimiter' do
|
19
7
|
limiter = Berater.new(:key) { 1.per second }
|
20
8
|
expect(limiter).to be_a Berater::RateLimiter
|
data/spec/dsl_spec.rb
CHANGED
@@ -13,13 +13,13 @@ describe Berater::DSL do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'parses' do
|
16
|
-
check([ 1, :second ]) { 1.per second }
|
17
|
-
check([ 3, :minute ]) { 3.per minute }
|
18
|
-
check([ 5, :hour ]) { 5.every hour }
|
16
|
+
check([ 1, interval: :second ]) { 1.per second }
|
17
|
+
check([ 3, interval: :minute ]) { 3.per minute }
|
18
|
+
check([ 5, interval: :hour ]) { 5.every hour }
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'cleans up afterward' do
|
22
|
-
check([ 1, :second ]) { 1.per second }
|
22
|
+
check([ 1, interval: :second ]) { 1.per second }
|
23
23
|
|
24
24
|
expect(Integer).not_to respond_to(:per)
|
25
25
|
expect(Integer).not_to respond_to(:every)
|
@@ -29,7 +29,7 @@ describe Berater::DSL do
|
|
29
29
|
count = 1
|
30
30
|
interval = :second
|
31
31
|
|
32
|
-
check([ count, interval ]) { count.per interval }
|
32
|
+
check([ count, interval: interval ]) { count.per interval }
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
@@ -57,16 +57,4 @@ describe Berater::DSL do
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
-
context 'unlimited mode' do
|
61
|
-
it 'has keywords' do
|
62
|
-
check(:unlimited) { unlimited }
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
context 'inhibited mode' do
|
67
|
-
it 'has keywords' do
|
68
|
-
check(:inhibited) { inhibited }
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
60
|
end
|
data/spec/inhibitor_spec.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
describe Berater::Inhibitor do
|
2
|
+
subject { described_class.new }
|
3
|
+
|
2
4
|
describe '.new' do
|
3
5
|
it 'initializes without any arguments or options' do
|
4
|
-
|
6
|
+
is_expected.to be_a described_class
|
5
7
|
end
|
6
8
|
|
7
9
|
it 'initializes with any arguments and options' do
|
@@ -9,15 +11,18 @@ describe Berater::Inhibitor do
|
|
9
11
|
end
|
10
12
|
|
11
13
|
it 'has default values' do
|
12
|
-
expect(
|
13
|
-
expect(
|
14
|
+
expect(subject.key).to be :inhibitor
|
15
|
+
expect(subject.redis).to be Berater.redis
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
17
19
|
describe '#limit' do
|
18
|
-
subject { described_class.new }
|
19
|
-
|
20
20
|
it_behaves_like 'it is overloaded'
|
21
21
|
end
|
22
22
|
|
23
|
+
describe '#to_s' do
|
24
|
+
it do
|
25
|
+
expect(subject.to_s).to include described_class.to_s
|
26
|
+
end
|
27
|
+
end
|
23
28
|
end
|
data/spec/limiter_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
describe Berater::Limiter do
|
2
2
|
it 'can not be initialized' do
|
3
|
-
expect { described_class.new }.to raise_error(
|
3
|
+
expect { described_class.new }.to raise_error(NoMethodError)
|
4
4
|
end
|
5
5
|
|
6
6
|
describe 'abstract methods' do
|
@@ -8,11 +8,51 @@ describe Berater::Limiter do
|
|
8
8
|
|
9
9
|
it do
|
10
10
|
expect { limiter.limit }.to raise_error(NotImplementedError)
|
11
|
-
expect { limiter.
|
11
|
+
expect { limiter.utilization }.to raise_error(NotImplementedError)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
describe '
|
15
|
+
describe '#limit' do
|
16
|
+
subject { Berater::Unlimiter.new }
|
17
|
+
|
18
|
+
context 'with a capacity parameter' do
|
19
|
+
it 'overrides the stored value' do
|
20
|
+
is_expected.to receive(:acquire_lock).with(3, anything)
|
21
|
+
|
22
|
+
subject.limit(capacity: 3)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'validates the type' do
|
26
|
+
expect {
|
27
|
+
subject.limit(capacity: 'abc')
|
28
|
+
}.to raise_error(ArgumentError)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'with a cost parameter' do
|
33
|
+
it 'overrides the stored value' do
|
34
|
+
is_expected.to receive(:acquire_lock).with(anything, 2)
|
35
|
+
|
36
|
+
subject.limit(cost: 2)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'validates' do
|
40
|
+
expect {
|
41
|
+
subject.limit(cost: 'abc')
|
42
|
+
}.to raise_error(ArgumentError)
|
43
|
+
|
44
|
+
expect {
|
45
|
+
subject.limit(cost: -1)
|
46
|
+
}.to raise_error(ArgumentError)
|
47
|
+
|
48
|
+
expect {
|
49
|
+
subject.limit(cost: Float::INFINITY)
|
50
|
+
}.to raise_error(ArgumentError)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#==' do
|
16
56
|
let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
|
17
57
|
|
18
58
|
it 'equals itself' do
|
@@ -25,12 +65,6 @@ describe Berater::Limiter do
|
|
25
65
|
)
|
26
66
|
end
|
27
67
|
|
28
|
-
it 'equals something with equvalent initialization parameters' do
|
29
|
-
expect(limiter).to eq(
|
30
|
-
Berater::RateLimiter.new(:key, 1, 1)
|
31
|
-
)
|
32
|
-
end
|
33
|
-
|
34
68
|
it 'does not equal something different' do
|
35
69
|
expect(limiter).not_to eq(
|
36
70
|
Berater::RateLimiter.new(:key, 2, :second)
|
@@ -68,4 +102,56 @@ describe Berater::Limiter do
|
|
68
102
|
end
|
69
103
|
end
|
70
104
|
|
105
|
+
describe '#cache_key' do
|
106
|
+
subject { klass.new(:key).send(:cache_key) }
|
107
|
+
|
108
|
+
context 'with Unlimiter' do
|
109
|
+
let(:klass) { Berater::Unlimiter }
|
110
|
+
|
111
|
+
it do
|
112
|
+
is_expected.to eq 'Berater:Unlimiter:key'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'with custom limiter' do
|
117
|
+
MyLimiter = Class.new(Berater::Unlimiter)
|
118
|
+
|
119
|
+
let(:klass) { MyLimiter }
|
120
|
+
|
121
|
+
it 'adds Berater prefix' do
|
122
|
+
is_expected.to eq 'Berater:MyLimiter:key'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe '.cache_key' do
|
128
|
+
subject { klass.send(:cache_key, :key) }
|
129
|
+
|
130
|
+
context 'with Unlimiter' do
|
131
|
+
let(:klass) { Berater::Unlimiter }
|
132
|
+
|
133
|
+
it do
|
134
|
+
is_expected.to eq 'Berater:Unlimiter:key'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context 'with custom limiter' do
|
139
|
+
MyLimiter = Class.new(Berater::Unlimiter)
|
140
|
+
|
141
|
+
let(:klass) { MyLimiter }
|
142
|
+
|
143
|
+
it 'adds Berater prefix' do
|
144
|
+
is_expected.to eq 'Berater:MyLimiter:key'
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe '.inherited' do
|
150
|
+
it 'creates convenience methods' do
|
151
|
+
expect(Berater.method(:Unlimiter)).to be_a Method
|
152
|
+
expect(Berater::Unlimiter()).to be_a Berater::Unlimiter
|
153
|
+
expect {|b| Berater::Unlimiter(&b) }.to yield_control
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
71
157
|
end
|