berater 0.4.0 → 0.5.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 +27 -38
- data/lib/berater/concurrency_limiter.rb +58 -44
- data/lib/berater/dsl.rb +20 -9
- data/lib/berater/inhibitor.rb +7 -2
- data/lib/berater/limiter.rb +48 -2
- data/lib/berater/lock.rb +1 -10
- data/lib/berater/lua_script.rb +55 -0
- data/lib/berater/rate_limiter.rb +42 -73
- data/lib/berater/rspec.rb +2 -0
- data/lib/berater/rspec/matchers.rb +8 -6
- data/lib/berater/test_mode.rb +14 -5
- data/lib/berater/unlimiter.rb +6 -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 +138 -63
- 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/matchers_spec.rb +14 -2
- data/spec/rate_limiter_spec.rb +94 -97
- data/spec/riddle_spec.rb +102 -0
- data/spec/test_mode_spec.rb +108 -78
- data/spec/unlimiter_spec.rb +3 -9
- data/spec/utils_spec.rb +78 -0
- metadata +31 -3
@@ -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 microsecond 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_usec)).to be (3 * 10**6)
|
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_usec)).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,153 @@ 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
|
+
it 'limit resets over time' do
|
92
|
+
2.times { limiter.limit }
|
93
|
+
expect(limiter).to be_incapacitated
|
103
94
|
|
104
|
-
|
95
|
+
Timecop.freeze(30)
|
105
96
|
|
106
|
-
|
107
|
-
expect(
|
97
|
+
2.times { limiter.limit }
|
98
|
+
expect(limiter).to be_incapacitated
|
99
|
+
end
|
108
100
|
|
109
|
-
|
110
|
-
|
101
|
+
it 'limit resets with millisecond precision' do
|
102
|
+
2.times { limiter.limit }
|
103
|
+
expect(limiter).to be_incapacitated
|
104
|
+
|
105
|
+
# travel forward to just before first lock times out
|
106
|
+
Timecop.freeze(29.999)
|
107
|
+
expect(limiter).to be_incapacitated
|
108
|
+
|
109
|
+
# traveling one more millisecond will decrement the count
|
110
|
+
Timecop.freeze(0.001)
|
111
|
+
2.times { limiter.limit }
|
112
|
+
expect(limiter).to be_incapacitated
|
111
113
|
end
|
112
|
-
end
|
113
114
|
|
114
|
-
|
115
|
-
|
116
|
-
|
115
|
+
context 'with cost parameter' do
|
116
|
+
it { expect { limiter.limit(cost: 4) }.to be_incapacitated }
|
117
|
+
|
118
|
+
it 'works within limit' do
|
119
|
+
limiter.limit(cost: 2)
|
120
|
+
expect(limiter).to be_incapacitated
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'releases full cost' do
|
124
|
+
lock = limiter.limit(cost: 2)
|
125
|
+
expect(limiter).to be_incapacitated
|
126
|
+
|
127
|
+
lock.release
|
128
|
+
expect(limiter).not_to be_incapacitated
|
129
|
+
|
130
|
+
lock = limiter.limit(cost: 2)
|
131
|
+
expect(limiter).to be_incapacitated
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'respects timeout' do
|
135
|
+
limiter.limit(cost: 2)
|
136
|
+
expect(limiter).to be_incapacitated
|
117
137
|
|
118
|
-
|
138
|
+
Timecop.freeze(30)
|
139
|
+
expect(limiter).not_to be_incapacitated
|
119
140
|
|
120
|
-
|
121
|
-
|
122
|
-
|
141
|
+
limiter.limit(cost: 2)
|
142
|
+
expect(limiter).to be_incapacitated
|
143
|
+
end
|
123
144
|
|
124
|
-
|
125
|
-
|
145
|
+
it 'accepts a dynamic capacity' do
|
146
|
+
limiter = described_class.new(:key, 1)
|
147
|
+
|
148
|
+
expect { limiter.limit(capacity: 0) }.to be_incapacitated
|
149
|
+
5.times { limiter.limit(capacity: 10) }
|
150
|
+
expect { limiter }.to be_incapacitated
|
151
|
+
end
|
152
|
+
end
|
126
153
|
|
127
|
-
|
128
|
-
|
154
|
+
context 'with same key, different limiters' do
|
155
|
+
let(:limiter_one) { described_class.new(:key, 1) }
|
156
|
+
let(:limiter_two) { described_class.new(:key, 1) }
|
129
157
|
|
130
|
-
expect(limiter_one).to
|
131
|
-
expect(limiter_two).to be_incapacitated
|
158
|
+
it { expect(limiter_one.key).to eq limiter_two.key }
|
132
159
|
|
133
|
-
|
134
|
-
|
135
|
-
expect(limiter_two).not_to be_incapacitated
|
160
|
+
it 'works as expected' do
|
161
|
+
expect(limiter_one.limit).to be_a Berater::Lock
|
136
162
|
|
137
|
-
|
138
|
-
|
139
|
-
|
163
|
+
expect(limiter_one).to be_incapacitated
|
164
|
+
expect(limiter_two).to be_incapacitated
|
165
|
+
end
|
140
166
|
end
|
141
|
-
end
|
142
167
|
|
143
|
-
|
144
|
-
|
145
|
-
|
168
|
+
context 'with same key, different capacities' do
|
169
|
+
let(:limiter_one) { described_class.new(:key, 1) }
|
170
|
+
let(:limiter_two) { described_class.new(:key, 2) }
|
171
|
+
|
172
|
+
it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
|
173
|
+
|
174
|
+
it 'works as expected' do
|
175
|
+
one_lock = limiter_one.limit
|
176
|
+
expect(one_lock).to be_a Berater::Lock
|
146
177
|
|
147
|
-
|
148
|
-
|
149
|
-
expect(limiter_two).not_to be_incapacitated
|
178
|
+
expect(limiter_one).to be_incapacitated
|
179
|
+
expect(limiter_two).not_to be_incapacitated
|
150
180
|
|
151
|
-
|
152
|
-
|
181
|
+
two_lock = limiter_two.limit
|
182
|
+
expect(two_lock).to be_a Berater::Lock
|
153
183
|
|
154
|
-
|
155
|
-
|
184
|
+
expect(limiter_one).to be_incapacitated
|
185
|
+
expect(limiter_two).to be_incapacitated
|
156
186
|
|
157
|
-
|
158
|
-
|
187
|
+
one_lock.release
|
188
|
+
expect(limiter_one).to be_incapacitated
|
189
|
+
expect(limiter_two).not_to be_incapacitated
|
190
|
+
|
191
|
+
two_lock.release
|
192
|
+
expect(limiter_one).not_to be_incapacitated
|
193
|
+
expect(limiter_two).not_to be_incapacitated
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context 'with different keys, different limiters' do
|
198
|
+
let(:limiter_one) { described_class.new(:one, 1) }
|
199
|
+
let(:limiter_two) { described_class.new(:two, 1) }
|
200
|
+
|
201
|
+
it 'works as expected' do
|
202
|
+
expect(limiter_one).not_to be_incapacitated
|
203
|
+
expect(limiter_two).not_to be_incapacitated
|
204
|
+
|
205
|
+
one_lock = limiter_one.limit
|
206
|
+
expect(one_lock).to be_a Berater::Lock
|
207
|
+
|
208
|
+
expect(limiter_one).to be_incapacitated
|
209
|
+
expect(limiter_two).not_to be_incapacitated
|
210
|
+
|
211
|
+
two_lock = limiter_two.limit
|
212
|
+
expect(two_lock).to be_a Berater::Lock
|
213
|
+
|
214
|
+
expect(limiter_one).to be_incapacitated
|
215
|
+
expect(limiter_two).to be_incapacitated
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
describe '#overloaded?' do
|
221
|
+
let(:limiter) { described_class.new(:key, 1, timeout: 30) }
|
222
|
+
|
223
|
+
it 'works' do
|
224
|
+
expect(limiter.overloaded?).to be false
|
225
|
+
lock = limiter.limit
|
226
|
+
expect(limiter.overloaded?).to be true
|
227
|
+
lock.release
|
228
|
+
expect(limiter.overloaded?).to be false
|
229
|
+
end
|
159
230
|
|
160
|
-
|
161
|
-
expect(
|
231
|
+
it 'respects timeout' do
|
232
|
+
expect(limiter.overloaded?).to be false
|
233
|
+
lock = limiter.limit
|
234
|
+
expect(limiter.overloaded?).to be true
|
235
|
+
Timecop.freeze(30)
|
236
|
+
expect(limiter.overloaded?).to be false
|
162
237
|
end
|
163
238
|
end
|
164
239
|
|
@@ -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
|
@@ -0,0 +1,71 @@
|
|
1
|
+
describe Berater::Limiter do
|
2
|
+
it 'can not be initialized' do
|
3
|
+
expect { described_class.new }.to raise_error(NotImplementedError)
|
4
|
+
end
|
5
|
+
|
6
|
+
describe 'abstract methods' do
|
7
|
+
let(:limiter) { Class.new(described_class).new(:key, 1) }
|
8
|
+
|
9
|
+
it do
|
10
|
+
expect { limiter.limit }.to raise_error(NotImplementedError)
|
11
|
+
expect { limiter.overloaded? }.to raise_error(NotImplementedError)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '==' do
|
16
|
+
let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
|
17
|
+
|
18
|
+
it 'equals itself' do
|
19
|
+
expect(limiter).to eq limiter
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'equals something with the same initialization parameters' do
|
23
|
+
expect(limiter).to eq(
|
24
|
+
Berater::RateLimiter.new(:key, 1, :second)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
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
|
+
it 'does not equal something different' do
|
35
|
+
expect(limiter).not_to eq(
|
36
|
+
Berater::RateLimiter.new(:key, 2, :second)
|
37
|
+
)
|
38
|
+
|
39
|
+
expect(limiter).not_to eq(
|
40
|
+
Berater::RateLimiter.new(:keyz, 1, :second)
|
41
|
+
)
|
42
|
+
|
43
|
+
expect(limiter).not_to eq(
|
44
|
+
Berater::RateLimiter.new(:key, 1, :minute)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'does not equal something altogether different' do
|
49
|
+
expect(limiter).not_to eq(
|
50
|
+
Berater::ConcurrencyLimiter.new(:key, 1)
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'works for ConcurrencyLimiter too' do
|
55
|
+
limiter = Berater::ConcurrencyLimiter.new(:key, 1)
|
56
|
+
expect(limiter).to eq limiter
|
57
|
+
|
58
|
+
expect(limiter).not_to eq(
|
59
|
+
Berater::ConcurrencyLimiter.new(:key, 1, timeout: 1)
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'and the others' do
|
64
|
+
unlimiter = Berater::Unlimiter.new
|
65
|
+
expect(unlimiter).to eq unlimiter
|
66
|
+
|
67
|
+
expect(unlimiter).not_to eq Berater::Inhibitor.new
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
describe Berater::LuaScript do
|
2
|
+
subject { Berater::LuaScript('return redis.call("PING")') }
|
3
|
+
|
4
|
+
before { redis.script(:flush) }
|
5
|
+
|
6
|
+
let(:redis) { Berater.redis }
|
7
|
+
|
8
|
+
it { is_expected.to be_a Berater::LuaScript }
|
9
|
+
|
10
|
+
describe '#eval' do
|
11
|
+
def ping
|
12
|
+
expect(subject.eval(redis)).to eq 'PONG'
|
13
|
+
end
|
14
|
+
|
15
|
+
it { ping }
|
16
|
+
|
17
|
+
it 'loads the script into redis' do
|
18
|
+
expect(redis).to receive(:evalsha).once.and_call_original
|
19
|
+
expect(redis).to receive(:eval).once.and_call_original
|
20
|
+
ping
|
21
|
+
expect(subject.loaded?(redis)).to be true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#load' do
|
26
|
+
it 'loads script into redis' do
|
27
|
+
expect(redis.script(:exists, subject.sha)).to be false
|
28
|
+
subject.load(redis)
|
29
|
+
expect(redis.script(:exists, subject.sha)).to be true
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns the sha' do
|
33
|
+
expect(subject.load(redis)).to eq subject.sha
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'validates the returned sha' do
|
37
|
+
allow(redis).to receive(:script).with(:flush).and_call_original
|
38
|
+
expect(redis).to receive(:script).with(:load, String).and_return('abc')
|
39
|
+
expect { subject.load(redis) }.to raise_error(RuntimeError)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#loaded?' do
|
44
|
+
it do
|
45
|
+
expect(subject.loaded?(redis)).to be false
|
46
|
+
subject.load(redis)
|
47
|
+
expect(subject.loaded?(redis)).to be true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#to_s' do
|
52
|
+
it { expect(subject.to_s).to be subject.source }
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#minify' do
|
56
|
+
subject do
|
57
|
+
expect(
|
58
|
+
Berater::LuaScript(lua).send(:minify)
|
59
|
+
).to eq expected
|
60
|
+
end
|
61
|
+
|
62
|
+
context do
|
63
|
+
let(:lua) do <<-LUA
|
64
|
+
-- this comment gets removed
|
65
|
+
redis.call('PING') -- this one too
|
66
|
+
LUA
|
67
|
+
end
|
68
|
+
|
69
|
+
let(:expected) { "redis.call('PING')" }
|
70
|
+
|
71
|
+
it { subject }
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'with if statement' do
|
75
|
+
let(:lua) do <<~LUA
|
76
|
+
if condition then
|
77
|
+
call
|
78
|
+
end
|
79
|
+
|
80
|
+
return 123
|
81
|
+
LUA
|
82
|
+
end
|
83
|
+
|
84
|
+
let(:expected) do
|
85
|
+
[
|
86
|
+
'if condition then',
|
87
|
+
'call',
|
88
|
+
'end',
|
89
|
+
'return 123'
|
90
|
+
].join "\n"
|
91
|
+
end
|
92
|
+
|
93
|
+
it { subject }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|