berater 0.3.0 → 0.6.2
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 +27 -38
- data/lib/berater/concurrency_limiter.rb +53 -45
- data/lib/berater/dsl.rb +20 -9
- data/lib/berater/inhibitor.rb +4 -2
- data/lib/berater/limiter.rb +68 -4
- data/lib/berater/lock.rb +4 -14
- data/lib/berater/lua_script.rb +55 -0
- data/lib/berater/rate_limiter.rb +64 -63
- data/lib/berater/rspec.rb +3 -1
- data/lib/berater/rspec/matchers.rb +57 -36
- data/lib/berater/test_mode.rb +21 -21
- data/lib/berater/unlimiter.rb +8 -12
- data/lib/berater/utils.rb +46 -0
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +33 -70
- data/spec/concurrency_limiter_spec.rb +166 -64
- 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 +107 -0
- data/spec/lua_script_spec.rb +97 -0
- data/spec/matchers_spec.rb +71 -3
- data/spec/rate_limiter_spec.rb +132 -94
- data/spec/riddle_spec.rb +102 -0
- data/spec/test_mode_spec.rb +123 -81
- data/spec/unlimiter_spec.rb +3 -9
- data/spec/utils_spec.rb +78 -0
- metadata +31 -3
data/lib/berater/version.rb
CHANGED
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 '
|
10
|
+
it 'is used with a block' do
|
11
11
|
Berater.configure do |c|
|
12
12
|
c.redis = :redis
|
13
13
|
end
|
14
14
|
|
15
|
-
expect(Berater.redis).to
|
15
|
+
expect(Berater.redis).to be :redis
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -26,7 +26,7 @@ describe Berater do
|
|
26
26
|
|
27
27
|
describe '.new' do
|
28
28
|
context 'unlimited mode' do
|
29
|
-
let(:limiter) { Berater.new(:key,
|
29
|
+
let(:limiter) { Berater.new(:key, Float::INFINITY) }
|
30
30
|
|
31
31
|
it 'instantiates an Unlimiter' do
|
32
32
|
expect(limiter).to be_a Berater::Unlimiter
|
@@ -39,18 +39,13 @@ describe Berater do
|
|
39
39
|
|
40
40
|
it 'accepts options' do
|
41
41
|
redis = double('Redis')
|
42
|
-
limiter = Berater.new(:key,
|
42
|
+
limiter = Berater.new(:key, Float::INFINITY, redis: redis)
|
43
43
|
expect(limiter.redis).to be redis
|
44
44
|
end
|
45
|
-
|
46
|
-
it 'works with convinience' do
|
47
|
-
expect(Berater).to receive(:new).and_return(limiter)
|
48
|
-
expect {|b| Berater(:key, :unlimited, &b) }.to yield_control
|
49
|
-
end
|
50
45
|
end
|
51
46
|
|
52
47
|
context 'inhibited mode' do
|
53
|
-
let(:limiter) { Berater.new(:key,
|
48
|
+
let(:limiter) { Berater.new(:key, 0) }
|
54
49
|
|
55
50
|
it 'instantiates an Inhibitor' do
|
56
51
|
expect(limiter).to be_a Berater::Inhibitor
|
@@ -63,18 +58,13 @@ describe Berater do
|
|
63
58
|
|
64
59
|
it 'accepts options' do
|
65
60
|
redis = double('Redis')
|
66
|
-
limiter = Berater.new(:key,
|
61
|
+
limiter = Berater.new(:key, 0, redis: redis)
|
67
62
|
expect(limiter.redis).to be redis
|
68
63
|
end
|
69
|
-
|
70
|
-
it 'works with convinience' do
|
71
|
-
expect(Berater).to receive(:new).and_return(limiter)
|
72
|
-
expect { Berater(:key, :inhibited) }.to be_inhibited
|
73
|
-
end
|
74
64
|
end
|
75
65
|
|
76
66
|
context 'rate mode' do
|
77
|
-
let(:limiter) { Berater.new(:key,
|
67
|
+
let(:limiter) { Berater.new(:key, 1, :second) }
|
78
68
|
|
79
69
|
it 'instantiates a RateLimiter' do
|
80
70
|
expect(limiter).to be_a Berater::RateLimiter
|
@@ -87,18 +77,13 @@ describe Berater do
|
|
87
77
|
|
88
78
|
it 'accepts options' do
|
89
79
|
redis = double('Redis')
|
90
|
-
limiter = Berater.new(:key,
|
80
|
+
limiter = Berater.new(:key, 1, :second, redis: redis)
|
91
81
|
expect(limiter.redis).to be redis
|
92
82
|
end
|
93
|
-
|
94
|
-
it 'works with convinience' do
|
95
|
-
expect(Berater).to receive(:new).and_return(limiter)
|
96
|
-
expect {|b| Berater(:key, :rate, 1, :second, &b) }.to yield_control
|
97
|
-
end
|
98
83
|
end
|
99
84
|
|
100
85
|
context 'concurrency mode' do
|
101
|
-
let(:limiter) { Berater.new(:key,
|
86
|
+
let(:limiter) { Berater.new(:key, 1) }
|
102
87
|
|
103
88
|
it 'instantiates a ConcurrencyLimiter' do
|
104
89
|
expect(limiter).to be_a Berater::ConcurrencyLimiter
|
@@ -111,61 +96,39 @@ describe Berater do
|
|
111
96
|
|
112
97
|
it 'accepts options' do
|
113
98
|
redis = double('Redis')
|
114
|
-
limiter = Berater.new(:key,
|
99
|
+
limiter = Berater.new(:key, 1, redis: redis)
|
115
100
|
expect(limiter.redis).to be redis
|
116
101
|
end
|
117
|
-
|
118
|
-
it 'works with convinience' do
|
119
|
-
expect(Berater).to receive(:new).and_return(limiter)
|
120
|
-
expect {|b| Berater(:key, :concurrency, 1, &b) }.to yield_control
|
121
|
-
end
|
122
102
|
end
|
103
|
+
end
|
123
104
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
expect(limiter
|
129
|
-
end
|
130
|
-
|
131
|
-
it 'instatiates an Inhibiter' do
|
132
|
-
limiter = Berater.new(:key) { inhibited }
|
133
|
-
expect(limiter).to be_a Berater::Inhibitor
|
134
|
-
expect(limiter.key).to be :key
|
135
|
-
end
|
136
|
-
|
137
|
-
it 'instatiates a RateLimiter' do
|
138
|
-
limiter = Berater.new(:key) { 1.per second }
|
139
|
-
expect(limiter).to be_a Berater::RateLimiter
|
140
|
-
expect(limiter.key).to be :key
|
141
|
-
expect(limiter.count).to be 1
|
142
|
-
expect(limiter.interval).to be :second
|
143
|
-
end
|
144
|
-
|
145
|
-
it 'instatiates a ConcurrencyLimiter' do
|
146
|
-
limiter = Berater.new(:key, timeout: 2) { 1.at_once }
|
147
|
-
expect(limiter).to be_a Berater::ConcurrencyLimiter
|
148
|
-
expect(limiter.key).to be :key
|
149
|
-
expect(limiter.capacity).to be 1
|
150
|
-
expect(limiter.timeout).to be 2
|
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
|
151
110
|
end
|
152
111
|
|
153
|
-
|
154
|
-
|
155
|
-
Berater
|
156
|
-
|
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
|
157
117
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
Berater.new(:key)
|
166
|
-
}.to raise_error(ArgumentError)
|
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
|
167
125
|
end
|
168
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
|
169
132
|
end
|
170
133
|
|
171
134
|
end
|
@@ -23,6 +23,7 @@ describe Berater::ConcurrencyLimiter do
|
|
23
23
|
|
24
24
|
it { expect_capacity(0) }
|
25
25
|
it { expect_capacity(1) }
|
26
|
+
it { expect_capacity(1.5) }
|
26
27
|
it { expect_capacity(10_000) }
|
27
28
|
|
28
29
|
context 'with erroneous values' do
|
@@ -32,7 +33,6 @@ describe Berater::ConcurrencyLimiter do
|
|
32
33
|
end.to raise_error ArgumentError
|
33
34
|
end
|
34
35
|
|
35
|
-
it { expect_bad_capacity(0.5) }
|
36
36
|
it { expect_bad_capacity(-1) }
|
37
37
|
it { expect_bad_capacity('1') }
|
38
38
|
it { expect_bad_capacity(:one) }
|
@@ -40,31 +40,23 @@ describe Berater::ConcurrencyLimiter do
|
|
40
40
|
end
|
41
41
|
|
42
42
|
describe '#timeout' do
|
43
|
-
|
44
|
-
limiter = described_class.new(:key, 1, timeout: timeout)
|
45
|
-
expect(limiter.timeout).to eq timeout
|
46
|
-
end
|
47
|
-
|
48
|
-
it { expect_timeout(0) }
|
49
|
-
it { expect_timeout(1) }
|
50
|
-
it { expect_timeout(10_000) }
|
43
|
+
# see spec/utils_spec.rb
|
51
44
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
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
|
58
50
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
63
55
|
end
|
64
56
|
end
|
65
57
|
|
66
58
|
describe '#limit' do
|
67
|
-
let(:limiter) { described_class.new(:key, 2) }
|
59
|
+
let(:limiter) { described_class.new(:key, 2, timeout: 30) }
|
68
60
|
|
69
61
|
it 'works' do
|
70
62
|
expect {|b| limiter.limit(&b) }.to yield_control
|
@@ -95,70 +87,180 @@ describe Berater::ConcurrencyLimiter do
|
|
95
87
|
expect(limiter).to be_incapacitated
|
96
88
|
end
|
97
89
|
end
|
98
|
-
end
|
99
90
|
|
100
|
-
|
101
|
-
|
102
|
-
|
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 }
|
105
|
+
expect(limiter).to be_incapacitated
|
106
|
+
|
107
|
+
Timecop.freeze(30)
|
108
|
+
|
109
|
+
2.times { limiter.limit }
|
110
|
+
expect(limiter).to be_incapacitated
|
111
|
+
end
|
103
112
|
|
104
|
-
it
|
113
|
+
it 'limit resets with millisecond precision' do
|
114
|
+
2.times { limiter.limit }
|
115
|
+
expect(limiter).to be_incapacitated
|
105
116
|
|
106
|
-
|
107
|
-
|
117
|
+
# travel forward to just before first lock times out
|
118
|
+
Timecop.freeze(29.999)
|
119
|
+
expect(limiter).to be_incapacitated
|
108
120
|
|
109
|
-
|
110
|
-
|
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
|
111
125
|
end
|
112
|
-
end
|
113
126
|
|
114
|
-
|
115
|
-
|
116
|
-
|
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)
|
149
|
+
expect(limiter).to be_incapacitated
|
150
|
+
end
|
117
151
|
|
118
|
-
|
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
|
119
162
|
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
123
175
|
|
124
|
-
|
125
|
-
|
176
|
+
it 'only allows positive values' do
|
177
|
+
expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
|
178
|
+
end
|
179
|
+
end
|
126
180
|
|
127
|
-
|
128
|
-
|
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) }
|
129
184
|
|
130
|
-
expect(limiter_one).to
|
131
|
-
expect(limiter_two).to be_incapacitated
|
185
|
+
it { expect(limiter_one.key).to eq limiter_two.key }
|
132
186
|
|
133
|
-
|
134
|
-
|
135
|
-
expect(limiter_two).not_to be_incapacitated
|
187
|
+
it 'works as expected' do
|
188
|
+
expect(limiter_one.limit).to be_a Berater::Lock
|
136
189
|
|
137
|
-
|
138
|
-
|
139
|
-
|
190
|
+
expect(limiter_one).to be_incapacitated
|
191
|
+
expect(limiter_two).to be_incapacitated
|
192
|
+
end
|
140
193
|
end
|
141
|
-
end
|
142
194
|
|
143
|
-
|
144
|
-
|
145
|
-
|
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) }
|
198
|
+
|
199
|
+
it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
|
200
|
+
|
201
|
+
it 'works as expected' do
|
202
|
+
one_lock = limiter_one.limit
|
203
|
+
expect(one_lock).to be_a Berater::Lock
|
146
204
|
|
147
|
-
|
148
|
-
|
149
|
-
expect(limiter_two).not_to be_incapacitated
|
205
|
+
expect(limiter_one).to be_incapacitated
|
206
|
+
expect(limiter_two).not_to be_incapacitated
|
150
207
|
|
151
|
-
|
152
|
-
|
208
|
+
two_lock = limiter_two.limit
|
209
|
+
expect(two_lock).to be_a Berater::Lock
|
153
210
|
|
154
|
-
|
155
|
-
|
211
|
+
expect(limiter_one).to be_incapacitated
|
212
|
+
expect(limiter_two).to be_incapacitated
|
156
213
|
|
157
|
-
|
158
|
-
|
214
|
+
one_lock.release
|
215
|
+
expect(limiter_one).to be_incapacitated
|
216
|
+
expect(limiter_two).not_to be_incapacitated
|
217
|
+
|
218
|
+
two_lock.release
|
219
|
+
expect(limiter_one).not_to be_incapacitated
|
220
|
+
expect(limiter_two).not_to be_incapacitated
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
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
|
246
|
+
|
247
|
+
describe '#overloaded?' do
|
248
|
+
let(:limiter) { described_class.new(:key, 1, timeout: 30) }
|
249
|
+
|
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
|
159
257
|
|
160
|
-
|
161
|
-
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
|
162
264
|
end
|
163
265
|
end
|
164
266
|
|