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
@@ -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,107 @@
|
|
1
|
+
describe Berater::Limiter do
|
2
|
+
it 'can not be initialized' do
|
3
|
+
expect { described_class.new }.to raise_error(NoMethodError)
|
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 '#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
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#==' do
|
52
|
+
let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
|
53
|
+
|
54
|
+
it 'equals itself' do
|
55
|
+
expect(limiter).to eq limiter
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'equals something with the same initialization parameters' do
|
59
|
+
expect(limiter).to eq(
|
60
|
+
Berater::RateLimiter.new(:key, 1, :second)
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'equals something with equvalent initialization parameters' do
|
65
|
+
expect(limiter).to eq(
|
66
|
+
Berater::RateLimiter.new(:key, 1, 1)
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'does not equal something different' do
|
71
|
+
expect(limiter).not_to eq(
|
72
|
+
Berater::RateLimiter.new(:key, 2, :second)
|
73
|
+
)
|
74
|
+
|
75
|
+
expect(limiter).not_to eq(
|
76
|
+
Berater::RateLimiter.new(:keyz, 1, :second)
|
77
|
+
)
|
78
|
+
|
79
|
+
expect(limiter).not_to eq(
|
80
|
+
Berater::RateLimiter.new(:key, 1, :minute)
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'does not equal something altogether different' do
|
85
|
+
expect(limiter).not_to eq(
|
86
|
+
Berater::ConcurrencyLimiter.new(:key, 1)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'works for ConcurrencyLimiter too' do
|
91
|
+
limiter = Berater::ConcurrencyLimiter.new(:key, 1)
|
92
|
+
expect(limiter).to eq limiter
|
93
|
+
|
94
|
+
expect(limiter).not_to eq(
|
95
|
+
Berater::ConcurrencyLimiter.new(:key, 1, timeout: 1)
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'and the others' do
|
100
|
+
unlimiter = Berater::Unlimiter.new
|
101
|
+
expect(unlimiter).to eq unlimiter
|
102
|
+
|
103
|
+
expect(unlimiter).not_to eq Berater::Inhibitor.new
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
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
|
data/spec/matchers_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
describe
|
1
|
+
describe Berater::Matchers::Overloaded do
|
2
|
+
|
2
3
|
context 'Berater::Unlimiter' do
|
3
4
|
let(:limiter) { Berater.new(:key, :unlimited) }
|
4
5
|
|
@@ -32,7 +33,7 @@ describe 'be_overloaded' do
|
|
32
33
|
end
|
33
34
|
|
34
35
|
context 'Berater::RateLimiter' do
|
35
|
-
let(:limiter) { Berater.new(:key,
|
36
|
+
let(:limiter) { Berater.new(:key, 1, :second) }
|
36
37
|
|
37
38
|
it { expect(limiter).not_to be_overloaded }
|
38
39
|
it { expect(limiter).not_to be_inhibited }
|
@@ -67,7 +68,7 @@ describe 'be_overloaded' do
|
|
67
68
|
end
|
68
69
|
|
69
70
|
context 'Berater::ConcurrencyLimiter' do
|
70
|
-
let(:limiter) { Berater.new(:key,
|
71
|
+
let(:limiter) { Berater.new(:key, 1) }
|
71
72
|
|
72
73
|
it { expect(limiter).not_to be_overloaded }
|
73
74
|
it { expect(limiter).not_to be_inhibited }
|
@@ -115,4 +116,71 @@ describe 'be_overloaded' do
|
|
115
116
|
end
|
116
117
|
end
|
117
118
|
end
|
119
|
+
|
120
|
+
context 'when matchers fail' do
|
121
|
+
let(:unlimiter) { Berater::Unlimiter.new }
|
122
|
+
let(:inhibitor) { Berater::Inhibitor.new }
|
123
|
+
|
124
|
+
it 'catches false negatives' do
|
125
|
+
expect {
|
126
|
+
expect(unlimiter).to be_overloaded
|
127
|
+
}.to fail_including('expected to be overloaded')
|
128
|
+
|
129
|
+
expect {
|
130
|
+
expect { unlimiter }.to be_overloaded
|
131
|
+
}.to fail_including('expected to be overloaded')
|
132
|
+
|
133
|
+
expect {
|
134
|
+
expect { unlimiter.limit }.to be_overloaded
|
135
|
+
}.to fail_including("expected #{Berater::Overloaded} to be raised")
|
136
|
+
|
137
|
+
expect {
|
138
|
+
expect { 123 }.to be_overloaded
|
139
|
+
}.to fail_including("expected #{Berater::Overloaded} to be raised")
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'catches false positives' do
|
143
|
+
expect {
|
144
|
+
expect(inhibitor).not_to be_overloaded
|
145
|
+
}.to fail_including('expected not to be overloaded')
|
146
|
+
|
147
|
+
expect {
|
148
|
+
expect { inhibitor }.not_to be_overloaded
|
149
|
+
}.to fail_including('expected not to be overloaded')
|
150
|
+
|
151
|
+
expect {
|
152
|
+
expect { inhibitor.limit }.not_to be_overloaded
|
153
|
+
}.to fail_including("did not expect #{Berater::Overloaded} to be raised")
|
154
|
+
|
155
|
+
expect {
|
156
|
+
expect { raise Berater::Overloaded }.not_to be_overloaded
|
157
|
+
}.to fail_including("did not expect #{Berater::Overloaded} to be raised")
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'supports different verbs' do
|
161
|
+
expect {
|
162
|
+
expect { unlimiter }.to be_overrated
|
163
|
+
}.to fail_including('expected to be overrated')
|
164
|
+
|
165
|
+
expect {
|
166
|
+
expect { unlimiter }.to be_incapacitated
|
167
|
+
}.to fail_including('expected to be incapacitated')
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'supports different exceptions' do
|
171
|
+
expect {
|
172
|
+
expect { 123 }.to be_overrated
|
173
|
+
}.to fail_including(
|
174
|
+
"expected #{Berater::RateLimiter::Overrated} to be raised"
|
175
|
+
)
|
176
|
+
|
177
|
+
expect {
|
178
|
+
expect {
|
179
|
+
raise Berater::ConcurrencyLimiter::Incapacitated
|
180
|
+
}.not_to be_incapacitated
|
181
|
+
}.to fail_including(
|
182
|
+
"did not expect #{Berater::ConcurrencyLimiter::Incapacitated} to be raised"
|
183
|
+
)
|
184
|
+
end
|
185
|
+
end
|
118
186
|
end
|
data/spec/rate_limiter_spec.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
describe Berater::RateLimiter do
|
2
|
-
it_behaves_like 'a limiter', Berater.new(:key,
|
2
|
+
it_behaves_like 'a limiter', Berater.new(:key, 3, :second)
|
3
3
|
|
4
4
|
describe '.new' do
|
5
5
|
let(:limiter) { described_class.new(:key, 1, :second) }
|
6
6
|
|
7
7
|
it 'initializes' do
|
8
8
|
expect(limiter.key).to be :key
|
9
|
-
expect(limiter.
|
9
|
+
expect(limiter.capacity).to eq 1
|
10
10
|
expect(limiter.interval).to eq :second
|
11
11
|
end
|
12
12
|
|
@@ -15,82 +15,48 @@ describe Berater::RateLimiter do
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
describe '#
|
19
|
-
def
|
20
|
-
limiter = described_class.new(:key,
|
21
|
-
expect(limiter.
|
18
|
+
describe '#capacity' do
|
19
|
+
def expect_capacity(capacity)
|
20
|
+
limiter = described_class.new(:key, capacity, :second)
|
21
|
+
expect(limiter.capacity).to eq capacity
|
22
22
|
end
|
23
23
|
|
24
|
-
it {
|
25
|
-
it {
|
26
|
-
it {
|
24
|
+
it { expect_capacity(0) }
|
25
|
+
it { expect_capacity(1) }
|
26
|
+
it { expect_capacity(1.5) }
|
27
|
+
it { expect_capacity(100) }
|
27
28
|
|
28
29
|
context 'with erroneous values' do
|
29
|
-
def
|
30
|
+
def expect_bad_capacity(capacity)
|
30
31
|
expect do
|
31
|
-
described_class.new(:key,
|
32
|
+
described_class.new(:key, capacity, :second)
|
32
33
|
end.to raise_error ArgumentError
|
33
34
|
end
|
34
35
|
|
35
|
-
it {
|
36
|
-
it {
|
37
|
-
it {
|
38
|
-
it { expect_bad_count(:one) }
|
36
|
+
it { expect_bad_capacity(-1) }
|
37
|
+
it { expect_bad_capacity('1') }
|
38
|
+
it { expect_bad_capacity(:one) }
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
42
|
describe '#interval' do
|
43
|
-
|
44
|
-
limiter = described_class.new(:key, 1, interval)
|
45
|
-
expect(limiter.interval).to eq expected
|
46
|
-
end
|
47
|
-
|
48
|
-
context 'with ints' do
|
49
|
-
it { expect_interval(0, 0) }
|
50
|
-
it { expect_interval(1, 1) }
|
51
|
-
it { expect_interval(33, 33) }
|
52
|
-
end
|
53
|
-
|
54
|
-
context 'with symbols' do
|
55
|
-
it { expect_interval(:sec, :second) }
|
56
|
-
it { expect_interval(:second, :second) }
|
57
|
-
it { expect_interval(:seconds, :second) }
|
58
|
-
|
59
|
-
it { expect_interval(:min, :minute) }
|
60
|
-
it { expect_interval(:minute, :minute) }
|
61
|
-
it { expect_interval(:minutes, :minute) }
|
62
|
-
|
63
|
-
it { expect_interval(:hour, :hour) }
|
64
|
-
it { expect_interval(:hours, :hour) }
|
65
|
-
end
|
43
|
+
# see spec/utils_spec.rb for more
|
66
44
|
|
67
|
-
|
68
|
-
it { expect_interval('sec', :second) }
|
69
|
-
it { expect_interval('minute', :minute) }
|
70
|
-
it { expect_interval('hours', :hour) }
|
71
|
-
end
|
45
|
+
subject { described_class.new(:key, 1, :second) }
|
72
46
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
described_class.new(:key, 1, interval)
|
77
|
-
end.to raise_error(ArgumentError)
|
78
|
-
end
|
79
|
-
|
80
|
-
it { expect_bad_interval(-1) }
|
81
|
-
it { expect_bad_interval(:secondz) }
|
82
|
-
it { expect_bad_interval('huor') }
|
47
|
+
it 'saves the interval in original and millisecond format' do
|
48
|
+
expect(subject.interval).to be :second
|
49
|
+
expect(subject.instance_variable_get(:@interval_msec)).to be 10**3
|
83
50
|
end
|
84
51
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
end
|
52
|
+
it 'must be > 0' do
|
53
|
+
expect {
|
54
|
+
described_class.new(:key, 1, 0)
|
55
|
+
}.to raise_error(ArgumentError)
|
90
56
|
|
91
|
-
|
92
|
-
|
93
|
-
|
57
|
+
expect {
|
58
|
+
described_class.new(:key, 1, -1)
|
59
|
+
}.to raise_error(ArgumentError)
|
94
60
|
end
|
95
61
|
end
|
96
62
|
|
@@ -112,50 +78,132 @@ describe Berater::RateLimiter do
|
|
112
78
|
expect(limiter).to be_overrated
|
113
79
|
end
|
114
80
|
|
115
|
-
it 'limit
|
81
|
+
it 'resets limit over time' do
|
116
82
|
3.times { limiter.limit }
|
117
83
|
expect(limiter).to be_overrated
|
118
84
|
|
119
|
-
# travel forward a second
|
120
85
|
Timecop.freeze(1)
|
121
86
|
|
122
87
|
3.times { limiter.limit }
|
123
88
|
expect(limiter).to be_overrated
|
124
89
|
end
|
125
|
-
end
|
126
90
|
|
127
|
-
|
128
|
-
|
129
|
-
|
91
|
+
context 'with millisecond precision' do
|
92
|
+
it 'resets limit over time' do
|
93
|
+
3.times { limiter.limit }
|
94
|
+
expect(limiter).to be_overrated
|
130
95
|
|
131
|
-
|
132
|
-
|
96
|
+
# travel forward to just before the count decrements
|
97
|
+
Timecop.freeze(0.333)
|
98
|
+
expect(limiter).to be_overrated
|
133
99
|
|
134
|
-
|
135
|
-
|
100
|
+
# traveling one more millisecond will decrement the count
|
101
|
+
Timecop.freeze(0.001)
|
102
|
+
limiter.limit
|
103
|
+
expect(limiter).to be_overrated
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'works when drip rate is < 1 per millisecond' do
|
107
|
+
limiter = described_class.new(:key, 2_000, :second)
|
108
|
+
|
109
|
+
limiter.capacity.times { limiter.limit }
|
110
|
+
expect(limiter).to be_overrated
|
111
|
+
|
112
|
+
Timecop.freeze(0.001)
|
113
|
+
expect(limiter).not_to be_overrated
|
114
|
+
|
115
|
+
2.times { limiter.limit }
|
116
|
+
end
|
136
117
|
end
|
137
|
-
end
|
138
118
|
|
139
|
-
|
140
|
-
|
141
|
-
|
119
|
+
context 'when capacity is a Float' do
|
120
|
+
let(:limiter) { described_class.new(:key, 1.5, :second) }
|
121
|
+
|
122
|
+
it 'still works' do
|
123
|
+
limiter.limit
|
124
|
+
expect(limiter).not_to be_overrated
|
125
|
+
|
126
|
+
expect { limiter.limit }.to be_overrated
|
127
|
+
|
128
|
+
limiter.limit(cost: 0.5)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'accepts a dynamic capacity' do
|
133
|
+
limiter = described_class.new(:key, 1, :second)
|
134
|
+
|
135
|
+
expect { limiter.limit(capacity: 0) }.to be_overrated
|
136
|
+
5.times { limiter.limit(capacity: 10) }
|
137
|
+
expect { limiter }.to be_overrated
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'works with cost parameter' do
|
141
|
+
it { expect { limiter.limit(cost: 4) }.to be_overrated }
|
142
|
+
|
143
|
+
it 'works within limit' do
|
144
|
+
limiter.limit(cost: 3)
|
145
|
+
expect { limiter.limit }.to be_overrated
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'resets over time' do
|
149
|
+
limiter.limit(cost: 3)
|
150
|
+
expect(limiter).to be_overrated
|
151
|
+
|
152
|
+
Timecop.freeze(1)
|
153
|
+
expect(limiter).not_to be_overrated
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'can be a Float' do
|
157
|
+
2.times { limiter.limit(cost: 1.5) }
|
158
|
+
expect(limiter).to be_overrated
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context 'with same key, different limiters' do
|
163
|
+
let(:limiter_one) { described_class.new(:key, 1, :second) }
|
164
|
+
let(:limiter_two) { described_class.new(:key, 1, :second) }
|
165
|
+
|
166
|
+
it 'works as expected' do
|
167
|
+
expect(limiter_one.limit).not_to be_overrated
|
168
|
+
|
169
|
+
expect(limiter_one).to be_overrated
|
170
|
+
expect(limiter_two).to be_overrated
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context 'with different keys, different limiters' do
|
175
|
+
let(:limiter_one) { described_class.new(:one, 1, :second) }
|
176
|
+
let(:limiter_two) { described_class.new(:two, 2, :second) }
|
177
|
+
|
178
|
+
it 'works as expected' do
|
179
|
+
expect(limiter_one.limit).not_to be_overrated
|
180
|
+
expect(limiter_two.limit).not_to be_overrated
|
181
|
+
|
182
|
+
expect(limiter_one).to be_overrated
|
183
|
+
expect(limiter_two.limit).not_to be_overrated
|
142
184
|
|
143
|
-
|
144
|
-
|
145
|
-
|
185
|
+
expect(limiter_one).to be_overrated
|
186
|
+
expect(limiter_two).to be_overrated
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
146
190
|
|
147
|
-
|
148
|
-
|
191
|
+
describe '#overloaded?' do
|
192
|
+
let(:limiter) { described_class.new(:key, 1, :second) }
|
149
193
|
|
150
|
-
|
151
|
-
expect(
|
194
|
+
it 'works' do
|
195
|
+
expect(limiter.overloaded?).to be false
|
196
|
+
limiter.limit
|
197
|
+
expect(limiter.overloaded?).to be true
|
198
|
+
Timecop.freeze(1)
|
199
|
+
expect(limiter.overloaded?).to be false
|
152
200
|
end
|
153
201
|
end
|
154
202
|
|
155
203
|
describe '#to_s' do
|
156
|
-
def check(
|
204
|
+
def check(capacity, interval, expected)
|
157
205
|
expect(
|
158
|
-
described_class.new(:key,
|
206
|
+
described_class.new(:key, capacity, interval).to_s
|
159
207
|
).to match(expected)
|
160
208
|
end
|
161
209
|
|
@@ -171,16 +219,6 @@ describe Berater::RateLimiter do
|
|
171
219
|
check(1, 'hour', /1 per hour/)
|
172
220
|
end
|
173
221
|
|
174
|
-
it 'normalizes' do
|
175
|
-
check(1, :sec, /1 per second/)
|
176
|
-
check(1, :seconds, /1 per second/)
|
177
|
-
|
178
|
-
check(1, :min, /1 per minute/)
|
179
|
-
check(1, :minutes, /1 per minute/)
|
180
|
-
|
181
|
-
check(1, :hours, /1 per hour/)
|
182
|
-
end
|
183
|
-
|
184
222
|
it 'works with integers' do
|
185
223
|
check(1, 1, /1 every second/)
|
186
224
|
check(1, 2, /1 every 2 seconds/)
|