berater 0.2.0 → 0.6.1
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 +30 -23
- data/lib/berater/concurrency_limiter.rb +58 -46
- data/lib/berater/dsl.rb +68 -0
- data/lib/berater/inhibitor.rb +5 -3
- data/lib/berater/limiter.rb +94 -0
- data/lib/berater/lock.rb +4 -14
- data/lib/berater/lua_script.rb +55 -0
- data/lib/berater/rate_limiter.rb +69 -52
- data/lib/berater/rspec.rb +14 -0
- data/lib/berater/rspec/matchers.rb +81 -0
- data/lib/berater/test_mode.rb +43 -0
- data/lib/berater/unlimiter.rb +9 -14
- data/lib/berater/utils.rb +46 -0
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +37 -28
- data/spec/concurrency_limiter_spec.rb +179 -73
- data/spec/dsl_refinement_spec.rb +46 -0
- data/spec/dsl_spec.rb +72 -0
- data/spec/inhibitor_spec.rb +2 -4
- data/spec/limiter_spec.rb +71 -0
- data/spec/lua_script_spec.rb +97 -0
- data/spec/{matcher_spec.rb → matchers_spec.rb} +71 -3
- data/spec/rate_limiter_spec.rb +156 -70
- data/spec/riddle_spec.rb +102 -0
- data/spec/test_mode_spec.rb +225 -0
- data/spec/unlimiter_spec.rb +5 -12
- data/spec/utils_spec.rb +78 -0
- metadata +40 -10
- data/lib/berater/base_limiter.rb +0 -26
- data/spec/concurrency_lock_spec.rb +0 -39
- data/spec/rate_lock_spec.rb +0 -20
@@ -1,4 +1,7 @@
|
|
1
1
|
describe Berater::ConcurrencyLimiter do
|
2
|
+
it_behaves_like 'a limiter', described_class.new(:key, 1)
|
3
|
+
it_behaves_like 'a limiter', described_class.new(:key, 1, timeout: 1)
|
4
|
+
|
2
5
|
describe '.new' do
|
3
6
|
let(:limiter) { described_class.new(:key, 1) }
|
4
7
|
|
@@ -20,6 +23,7 @@ describe Berater::ConcurrencyLimiter do
|
|
20
23
|
|
21
24
|
it { expect_capacity(0) }
|
22
25
|
it { expect_capacity(1) }
|
26
|
+
it { expect_capacity(1.5) }
|
23
27
|
it { expect_capacity(10_000) }
|
24
28
|
|
25
29
|
context 'with erroneous values' do
|
@@ -29,7 +33,6 @@ describe Berater::ConcurrencyLimiter do
|
|
29
33
|
end.to raise_error ArgumentError
|
30
34
|
end
|
31
35
|
|
32
|
-
it { expect_bad_capacity(0.5) }
|
33
36
|
it { expect_bad_capacity(-1) }
|
34
37
|
it { expect_bad_capacity('1') }
|
35
38
|
it { expect_bad_capacity(:one) }
|
@@ -37,31 +40,23 @@ describe Berater::ConcurrencyLimiter do
|
|
37
40
|
end
|
38
41
|
|
39
42
|
describe '#timeout' do
|
40
|
-
|
41
|
-
limiter = described_class.new(:key, 1, timeout: timeout)
|
42
|
-
expect(limiter.timeout).to eq timeout
|
43
|
-
end
|
44
|
-
|
45
|
-
it { expect_timeout(0) }
|
46
|
-
it { expect_timeout(1) }
|
47
|
-
it { expect_timeout(10_000) }
|
43
|
+
# see spec/utils_spec.rb
|
48
44
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
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
|
55
50
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
60
55
|
end
|
61
56
|
end
|
62
57
|
|
63
58
|
describe '#limit' do
|
64
|
-
let(:limiter) { described_class.new(:key, 2, timeout:
|
59
|
+
let(:limiter) { described_class.new(:key, 2, timeout: 30) }
|
65
60
|
|
66
61
|
it 'works' do
|
67
62
|
expect {|b| limiter.limit(&b) }.to yield_control
|
@@ -85,89 +80,200 @@ describe Berater::ConcurrencyLimiter do
|
|
85
80
|
expect(limiter).to be_incapacitated
|
86
81
|
end
|
87
82
|
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
102
|
+
|
103
|
+
it 'limit resets over time' do
|
104
|
+
2.times { limiter.limit }
|
91
105
|
expect(limiter).to be_incapacitated
|
92
106
|
|
93
|
-
Timecop.
|
107
|
+
Timecop.freeze(30)
|
94
108
|
|
95
|
-
|
96
|
-
expect(limiter.limit).to be_a Berater::Lock
|
109
|
+
2.times { limiter.limit }
|
97
110
|
expect(limiter).to be_incapacitated
|
98
111
|
end
|
99
112
|
|
100
|
-
|
101
|
-
|
113
|
+
it 'limit resets with millisecond precision' do
|
114
|
+
2.times { limiter.limit }
|
115
|
+
expect(limiter).to be_incapacitated
|
102
116
|
|
103
|
-
|
117
|
+
# travel forward to just before first lock times out
|
118
|
+
Timecop.freeze(29.999)
|
119
|
+
expect(limiter).to be_incapacitated
|
120
|
+
|
121
|
+
# traveling one more millisecond will decrement the count
|
122
|
+
Timecop.freeze(0.001)
|
123
|
+
2.times { limiter.limit }
|
124
|
+
expect(limiter).to be_incapacitated
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'accepts a dynamic capacity' do
|
128
|
+
expect { limiter.limit(capacity: 0) }.to be_incapacitated
|
129
|
+
5.times { limiter.limit(capacity: 10) }
|
130
|
+
expect { limiter }.to be_incapacitated
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'with cost parameter' do
|
134
|
+
it { expect { limiter.limit(cost: 4) }.to be_incapacitated }
|
135
|
+
|
136
|
+
it 'works within limit' do
|
137
|
+
limiter.limit(cost: 2)
|
138
|
+
expect(limiter).to be_incapacitated
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'releases full cost' do
|
142
|
+
lock = limiter.limit(cost: 2)
|
143
|
+
expect(limiter).to be_incapacitated
|
144
|
+
|
145
|
+
lock.release
|
146
|
+
expect(limiter).not_to be_incapacitated
|
147
|
+
|
148
|
+
lock = limiter.limit(cost: 2)
|
104
149
|
expect(limiter).to be_incapacitated
|
105
150
|
end
|
151
|
+
|
152
|
+
it 'respects timeout' do
|
153
|
+
limiter.limit(cost: 2)
|
154
|
+
expect(limiter).to be_incapacitated
|
155
|
+
|
156
|
+
Timecop.freeze(30)
|
157
|
+
expect(limiter).not_to be_incapacitated
|
158
|
+
|
159
|
+
limiter.limit(cost: 2)
|
160
|
+
expect(limiter).to be_incapacitated
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'with fractional costs' do
|
164
|
+
it 'rounds up' do
|
165
|
+
limiter.limit(cost: 1.5)
|
166
|
+
expect(limiter).to be_incapacitated
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'accumulates correctly' do
|
170
|
+
limiter.limit(cost: 0.5) # => 1
|
171
|
+
limiter.limit(cost: 0.7) # => 2
|
172
|
+
expect(limiter).to be_incapacitated
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'only allows positive values' do
|
177
|
+
expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
|
178
|
+
end
|
106
179
|
end
|
107
|
-
end
|
108
180
|
|
109
|
-
|
110
|
-
|
111
|
-
|
181
|
+
context 'with same key, different limiters' do
|
182
|
+
let(:limiter_one) { described_class.new(:key, 1) }
|
183
|
+
let(:limiter_two) { described_class.new(:key, 1) }
|
112
184
|
|
113
|
-
|
185
|
+
it { expect(limiter_one.key).to eq limiter_two.key }
|
114
186
|
|
115
|
-
|
116
|
-
|
187
|
+
it 'works as expected' do
|
188
|
+
expect(limiter_one.limit).to be_a Berater::Lock
|
117
189
|
|
118
|
-
|
119
|
-
|
190
|
+
expect(limiter_one).to be_incapacitated
|
191
|
+
expect(limiter_two).to be_incapacitated
|
192
|
+
end
|
120
193
|
end
|
121
|
-
end
|
122
194
|
|
123
|
-
|
124
|
-
|
125
|
-
|
195
|
+
context 'with same key, different capacities' do
|
196
|
+
let(:limiter_one) { described_class.new(:key, 1) }
|
197
|
+
let(:limiter_two) { described_class.new(:key, 2) }
|
126
198
|
|
127
|
-
|
199
|
+
it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
|
128
200
|
|
129
|
-
|
130
|
-
|
131
|
-
|
201
|
+
it 'works as expected' do
|
202
|
+
one_lock = limiter_one.limit
|
203
|
+
expect(one_lock).to be_a Berater::Lock
|
132
204
|
|
133
|
-
|
134
|
-
|
205
|
+
expect(limiter_one).to be_incapacitated
|
206
|
+
expect(limiter_two).not_to be_incapacitated
|
135
207
|
|
136
|
-
|
137
|
-
|
208
|
+
two_lock = limiter_two.limit
|
209
|
+
expect(two_lock).to be_a Berater::Lock
|
138
210
|
|
139
|
-
|
140
|
-
|
211
|
+
expect(limiter_one).to be_incapacitated
|
212
|
+
expect(limiter_two).to be_incapacitated
|
141
213
|
|
142
|
-
|
143
|
-
|
144
|
-
|
214
|
+
one_lock.release
|
215
|
+
expect(limiter_one).to be_incapacitated
|
216
|
+
expect(limiter_two).not_to be_incapacitated
|
145
217
|
|
146
|
-
|
147
|
-
|
148
|
-
|
218
|
+
two_lock.release
|
219
|
+
expect(limiter_one).not_to be_incapacitated
|
220
|
+
expect(limiter_two).not_to be_incapacitated
|
221
|
+
end
|
149
222
|
end
|
150
|
-
end
|
151
223
|
|
152
|
-
|
153
|
-
|
154
|
-
|
224
|
+
context 'with different keys, different limiters' do
|
225
|
+
let(:limiter_one) { described_class.new(:one, 1) }
|
226
|
+
let(:limiter_two) { described_class.new(:two, 1) }
|
227
|
+
|
228
|
+
it 'works as expected' do
|
229
|
+
expect(limiter_one).not_to be_incapacitated
|
230
|
+
expect(limiter_two).not_to be_incapacitated
|
231
|
+
|
232
|
+
one_lock = limiter_one.limit
|
233
|
+
expect(one_lock).to be_a Berater::Lock
|
234
|
+
|
235
|
+
expect(limiter_one).to be_incapacitated
|
236
|
+
expect(limiter_two).not_to be_incapacitated
|
237
|
+
|
238
|
+
two_lock = limiter_two.limit
|
239
|
+
expect(two_lock).to be_a Berater::Lock
|
240
|
+
|
241
|
+
expect(limiter_one).to be_incapacitated
|
242
|
+
expect(limiter_two).to be_incapacitated
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
155
246
|
|
156
|
-
|
157
|
-
|
158
|
-
expect(limiter_two).not_to be_incapacitated
|
247
|
+
describe '#overloaded?' do
|
248
|
+
let(:limiter) { described_class.new(:key, 1, timeout: 30) }
|
159
249
|
|
160
|
-
|
161
|
-
expect(
|
250
|
+
it 'works' do
|
251
|
+
expect(limiter.overloaded?).to be false
|
252
|
+
lock = limiter.limit
|
253
|
+
expect(limiter.overloaded?).to be true
|
254
|
+
lock.release
|
255
|
+
expect(limiter.overloaded?).to be false
|
256
|
+
end
|
162
257
|
|
163
|
-
|
164
|
-
expect(
|
258
|
+
it 'respects timeout' do
|
259
|
+
expect(limiter.overloaded?).to be false
|
260
|
+
lock = limiter.limit
|
261
|
+
expect(limiter.overloaded?).to be true
|
262
|
+
Timecop.freeze(30)
|
263
|
+
expect(limiter.overloaded?).to be false
|
264
|
+
end
|
265
|
+
end
|
165
266
|
|
166
|
-
|
167
|
-
|
267
|
+
describe '#to_s' do
|
268
|
+
def check(capacity, expected)
|
269
|
+
expect(
|
270
|
+
described_class.new(:key, capacity).to_s
|
271
|
+
).to match(expected)
|
272
|
+
end
|
168
273
|
|
169
|
-
|
170
|
-
|
274
|
+
it 'works' do
|
275
|
+
check(1, /1 at a time/)
|
276
|
+
check(3, /3 at a time/)
|
171
277
|
end
|
172
278
|
end
|
173
279
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'berater/dsl'
|
2
|
+
|
3
|
+
describe Berater do
|
4
|
+
using Berater::DSL
|
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
|
+
it 'instatiates a RateLimiter' do
|
19
|
+
limiter = Berater.new(:key) { 1.per second }
|
20
|
+
expect(limiter).to be_a Berater::RateLimiter
|
21
|
+
expect(limiter.key).to be :key
|
22
|
+
expect(limiter.capacity).to be 1
|
23
|
+
expect(limiter.interval).to be :second
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'instatiates a ConcurrencyLimiter' do
|
27
|
+
limiter = Berater.new(:key, timeout: 2) { 1.at_once }
|
28
|
+
expect(limiter).to be_a Berater::ConcurrencyLimiter
|
29
|
+
expect(limiter.key).to be :key
|
30
|
+
expect(limiter.capacity).to be 1
|
31
|
+
expect(limiter.timeout).to be 2
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'does not accept args and dsl block' do
|
35
|
+
expect {
|
36
|
+
Berater.new(:key, 2) { 3.at_once }
|
37
|
+
}.to raise_error(ArgumentError)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'requires either mode or dsl block' do
|
41
|
+
expect {
|
42
|
+
Berater.new(:key)
|
43
|
+
}.to raise_error(ArgumentError)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/spec/dsl_spec.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'berater/dsl'
|
2
|
+
|
3
|
+
describe Berater::DSL do
|
4
|
+
def check(expected, &block)
|
5
|
+
expect(Berater::DSL.eval(&block)).to eq expected
|
6
|
+
end
|
7
|
+
|
8
|
+
context 'rate mode' do
|
9
|
+
it 'has keywords' do
|
10
|
+
check(:second) { second }
|
11
|
+
check(:minute) { minute }
|
12
|
+
check(:hour) { hour }
|
13
|
+
end
|
14
|
+
|
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 }
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'cleans up afterward' do
|
22
|
+
check([ 1, :second ]) { 1.per second }
|
23
|
+
|
24
|
+
expect(Integer).not_to respond_to(:per)
|
25
|
+
expect(Integer).not_to respond_to(:every)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'works with variables' do
|
29
|
+
count = 1
|
30
|
+
interval = :second
|
31
|
+
|
32
|
+
check([ count, interval ]) { count.per interval }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'concurrency mode' do
|
37
|
+
it 'parses' do
|
38
|
+
check([ 1 ]) { 1.at_once }
|
39
|
+
check([ 3 ]) { 3.at_a_time }
|
40
|
+
check([ 5 ]) { 5.concurrently }
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'cleans up afterward' do
|
44
|
+
check([ 1 ]) { 1.at_once }
|
45
|
+
|
46
|
+
expect(Integer).not_to respond_to(:at_once)
|
47
|
+
expect(Integer).not_to respond_to(:at_a_time)
|
48
|
+
expect(Integer).not_to respond_to(:concurrently)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'works with constants' do
|
52
|
+
class Foo
|
53
|
+
CAPACITY = 3
|
54
|
+
end
|
55
|
+
|
56
|
+
check([ Foo::CAPACITY ]) { Foo::CAPACITY.at_once }
|
57
|
+
end
|
58
|
+
end
|
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
|
+
end
|
data/spec/inhibitor_spec.rb
CHANGED
@@ -15,11 +15,9 @@ describe Berater::Inhibitor do
|
|
15
15
|
end
|
16
16
|
|
17
17
|
describe '#limit' do
|
18
|
-
|
18
|
+
subject { described_class.new }
|
19
19
|
|
20
|
-
|
21
|
-
expect { limiter.limit }.to be_inhibited
|
22
|
-
end
|
20
|
+
it_behaves_like 'it is overloaded'
|
23
21
|
end
|
24
22
|
|
25
23
|
end
|