berater 0.4.0 → 0.7.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 +29 -37
- data/lib/berater/concurrency_limiter.rb +53 -48
- data/lib/berater/dsl.rb +21 -10
- data/lib/berater/inhibitor.rb +3 -5
- data/lib/berater/limiter.rb +74 -4
- data/lib/berater/lock.rb +4 -14
- data/lib/berater/lua_script.rb +55 -0
- data/lib/berater/rate_limiter.rb +62 -90
- data/lib/berater/rspec.rb +3 -1
- data/lib/berater/rspec/matchers.rb +43 -42
- data/lib/berater/test_mode.rb +14 -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 +35 -72
- data/spec/concurrency_limiter_spec.rb +168 -66
- data/spec/dsl_refinement_spec.rb +34 -0
- data/spec/dsl_spec.rb +60 -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 +64 -60
- data/spec/rate_limiter_spec.rb +183 -96
- data/spec/riddle_spec.rb +106 -0
- data/spec/test_mode_spec.rb +83 -124
- 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
|
|
@@ -25,8 +25,8 @@ describe Berater do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
describe '.new' do
|
28
|
-
context '
|
29
|
-
let(:limiter) { Berater.new(:key,
|
28
|
+
context 'Unlimiter mode' do
|
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
|
-
context '
|
53
|
-
let(:limiter) { Berater.new(:key,
|
47
|
+
context 'Inhibitor mode' do
|
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, interval: :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, interval: :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, capacity, **opts|
|
107
|
+
it 'creates a limiter' do
|
108
|
+
limiter = Berater(:key, capacity, **opts)
|
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, capacity, **opts)
|
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, capacity, **opts) { 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, interval: :second
|
131
|
+
include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
|
169
132
|
end
|
170
133
|
|
171
134
|
end
|
@@ -18,11 +18,12 @@ 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) }
|
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
|
@@ -85,80 +77,190 @@ describe Berater::ConcurrencyLimiter do
|
|
85
77
|
expect(limiter.limit).to be_a Berater::Lock
|
86
78
|
expect(limiter.limit).to be_a Berater::Lock
|
87
79
|
|
88
|
-
expect(limiter).to
|
80
|
+
expect(limiter).to be_overloaded
|
89
81
|
end
|
90
82
|
|
91
83
|
context 'with capacity 0' do
|
92
84
|
let(:limiter) { described_class.new(:key, 0) }
|
93
85
|
|
94
86
|
it 'always fails' do
|
95
|
-
expect(limiter).to
|
87
|
+
expect(limiter).to be_overloaded
|
96
88
|
end
|
97
89
|
end
|
98
|
-
end
|
99
90
|
|
100
|
-
|
101
|
-
|
102
|
-
let(:limiter_two) { described_class.new(:key, 1) }
|
91
|
+
context 'when capacity is a Float' do
|
92
|
+
let(:limiter) { described_class.new(:key, 1.5) }
|
103
93
|
|
104
|
-
|
94
|
+
it 'still works' do
|
95
|
+
lock = limiter.limit
|
105
96
|
|
106
|
-
|
107
|
-
|
97
|
+
# since fractional cost is not supported
|
98
|
+
expect(lock.capacity).to be 1
|
99
|
+
expect(limiter).to be_overloaded
|
100
|
+
end
|
101
|
+
end
|
108
102
|
|
109
|
-
|
110
|
-
|
103
|
+
it 'limit resets over time' do
|
104
|
+
2.times { limiter.limit }
|
105
|
+
expect(limiter).to be_overloaded
|
106
|
+
|
107
|
+
Timecop.freeze(30)
|
108
|
+
|
109
|
+
2.times { limiter.limit }
|
110
|
+
expect(limiter).to be_overloaded
|
111
111
|
end
|
112
|
-
end
|
113
112
|
|
114
|
-
|
115
|
-
|
116
|
-
|
113
|
+
it 'limit resets with millisecond precision' do
|
114
|
+
2.times { limiter.limit }
|
115
|
+
expect(limiter).to be_overloaded
|
116
|
+
|
117
|
+
# travel forward to just before first lock times out
|
118
|
+
Timecop.freeze(29.999)
|
119
|
+
expect(limiter).to be_overloaded
|
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_overloaded
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'accepts a dynamic capacity' do
|
128
|
+
expect { limiter.limit(capacity: 0) }.to be_overloaded
|
129
|
+
5.times { limiter.limit(capacity: 10) }
|
130
|
+
expect { limiter }.to be_overloaded
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'with cost parameter' do
|
134
|
+
it { expect { limiter.limit(cost: 4) }.to be_overloaded }
|
135
|
+
|
136
|
+
it 'works within limit' do
|
137
|
+
limiter.limit(cost: 2)
|
138
|
+
expect(limiter).to be_overloaded
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'releases full cost' do
|
142
|
+
lock = limiter.limit(cost: 2)
|
143
|
+
expect(limiter).to be_overloaded
|
144
|
+
|
145
|
+
lock.release
|
146
|
+
expect(limiter).not_to be_overloaded
|
147
|
+
|
148
|
+
lock = limiter.limit(cost: 2)
|
149
|
+
expect(limiter).to be_overloaded
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'respects timeout' do
|
153
|
+
limiter.limit(cost: 2)
|
154
|
+
expect(limiter).to be_overloaded
|
117
155
|
|
118
|
-
|
156
|
+
Timecop.freeze(30)
|
157
|
+
expect(limiter).not_to be_overloaded
|
119
158
|
|
120
|
-
|
121
|
-
|
122
|
-
|
159
|
+
limiter.limit(cost: 2)
|
160
|
+
expect(limiter).to be_overloaded
|
161
|
+
end
|
123
162
|
|
124
|
-
|
125
|
-
|
163
|
+
context 'with fractional costs' do
|
164
|
+
it 'rounds up' do
|
165
|
+
limiter.limit(cost: 1.5)
|
166
|
+
expect(limiter).to be_overloaded
|
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_overloaded
|
173
|
+
end
|
174
|
+
end
|
126
175
|
|
127
|
-
|
128
|
-
|
176
|
+
it 'only allows positive values' do
|
177
|
+
expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
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_overloaded
|
191
|
+
expect(limiter_two).to be_overloaded
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
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
|
204
|
+
|
205
|
+
expect(limiter_one).to be_overloaded
|
206
|
+
expect(limiter_two).not_to be_overloaded
|
207
|
+
|
208
|
+
two_lock = limiter_two.limit
|
209
|
+
expect(two_lock).to be_a Berater::Lock
|
210
|
+
|
211
|
+
expect(limiter_one).to be_overloaded
|
212
|
+
expect(limiter_two).to be_overloaded
|
213
|
+
|
214
|
+
one_lock.release
|
215
|
+
expect(limiter_one).to be_overloaded
|
216
|
+
expect(limiter_two).not_to be_overloaded
|
217
|
+
|
218
|
+
two_lock.release
|
219
|
+
expect(limiter_one).not_to be_overloaded
|
220
|
+
expect(limiter_two).not_to be_overloaded
|
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_overloaded
|
230
|
+
expect(limiter_two).not_to be_overloaded
|
231
|
+
|
232
|
+
one_lock = limiter_one.limit
|
233
|
+
expect(one_lock).to be_a Berater::Lock
|
234
|
+
|
235
|
+
expect(limiter_one).to be_overloaded
|
236
|
+
expect(limiter_two).not_to be_overloaded
|
237
|
+
|
238
|
+
two_lock = limiter_two.limit
|
239
|
+
expect(two_lock).to be_a Berater::Lock
|
240
|
+
|
241
|
+
expect(limiter_one).to be_overloaded
|
242
|
+
expect(limiter_two).to be_overloaded
|
243
|
+
end
|
140
244
|
end
|
141
245
|
end
|
142
246
|
|
143
|
-
|
144
|
-
let(:
|
145
|
-
|
247
|
+
describe '#utilization' do
|
248
|
+
let(:limiter) { described_class.new(:key, 10, timeout: 30) }
|
249
|
+
|
250
|
+
it 'works' do
|
251
|
+
expect(limiter.utilization).to be 0.0
|
146
252
|
|
147
|
-
|
148
|
-
expect(
|
149
|
-
expect(limiter_two).not_to be_incapacitated
|
253
|
+
2.times { limiter.limit }
|
254
|
+
expect(limiter.utilization).to be 0.2
|
150
255
|
|
151
|
-
|
152
|
-
expect(one_lock).to be_a Berater::Lock
|
256
|
+
Timecop.freeze(15)
|
153
257
|
|
154
|
-
|
155
|
-
expect(
|
258
|
+
8.times { limiter.limit }
|
259
|
+
expect(limiter.utilization).to be 1.0
|
156
260
|
|
157
|
-
|
158
|
-
expect(two_lock).to be_a Berater::Lock
|
261
|
+
Timecop.freeze(15)
|
159
262
|
|
160
|
-
expect(
|
161
|
-
expect(limiter_two).to be_incapacitated
|
263
|
+
expect(limiter.utilization).to be 0.8
|
162
264
|
end
|
163
265
|
end
|
164
266
|
|