berater 0.1.4 → 0.6.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 +33 -35
- data/lib/berater/concurrency_limiter.rb +68 -110
- data/lib/berater/dsl.rb +68 -0
- data/lib/berater/inhibitor.rb +9 -10
- data/lib/berater/limiter.rb +80 -0
- data/lib/berater/lock.rb +26 -0
- data/lib/berater/lua_script.rb +55 -0
- data/lib/berater/rate_limiter.rb +77 -54
- data/lib/berater/rspec.rb +14 -0
- data/lib/berater/rspec/matchers.rb +83 -0
- data/lib/berater/test_mode.rb +52 -0
- data/lib/berater/unlimiter.rb +11 -9
- data/lib/berater/utils.rb +46 -0
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +43 -101
- data/spec/concurrency_limiter_spec.rb +168 -100
- data/spec/dsl_refinement_spec.rb +46 -0
- data/spec/dsl_spec.rb +72 -0
- data/spec/inhibitor_spec.rb +4 -18
- data/spec/limiter_spec.rb +71 -0
- data/spec/lua_script_spec.rb +97 -0
- data/spec/{matcher_spec.rb → matchers_spec.rb} +73 -5
- data/spec/rate_limiter_spec.rb +162 -99
- data/spec/riddle_spec.rb +102 -0
- data/spec/test_mode_spec.rb +206 -0
- data/spec/unlimiter_spec.rb +6 -37
- data/spec/utils_spec.rb +78 -0
- metadata +41 -8
- data/lib/berater/base_limiter.rb +0 -32
- data/spec/concurrency_lock_spec.rb +0 -92
@@ -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
@@ -1,37 +1,23 @@
|
|
1
1
|
describe Berater::Inhibitor do
|
2
|
-
before { Berater.mode = :inhibited }
|
3
|
-
|
4
2
|
describe '.new' do
|
5
3
|
it 'initializes without any arguments or options' do
|
6
4
|
expect(described_class.new).to be_a described_class
|
7
5
|
end
|
8
6
|
|
9
7
|
it 'initializes with any arguments and options' do
|
10
|
-
expect(described_class.new(:abc, x: 123)).to be_a described_class
|
8
|
+
expect(described_class.new(:abc, :def, x: 123)).to be_a described_class
|
11
9
|
end
|
12
10
|
|
13
11
|
it 'has default values' do
|
14
|
-
expect(described_class.new.key).to
|
12
|
+
expect(described_class.new.key).to be :inhibitor
|
15
13
|
expect(described_class.new.redis).to be Berater.redis
|
16
14
|
end
|
17
15
|
end
|
18
16
|
|
19
|
-
describe '.limit' do
|
20
|
-
it 'always limits' do
|
21
|
-
expect { described_class.limit }.to be_inhibited
|
22
|
-
end
|
23
|
-
|
24
|
-
it 'works with any arguments or options' do
|
25
|
-
expect { described_class.limit(:abc, x: 123) }.to be_inhibited
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
17
|
describe '#limit' do
|
30
|
-
|
18
|
+
subject { described_class.new }
|
31
19
|
|
32
|
-
|
33
|
-
expect { described_class.limit }.to be_inhibited
|
34
|
-
end
|
20
|
+
it_behaves_like 'it is overloaded'
|
35
21
|
end
|
36
22
|
|
37
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
|
@@ -1,6 +1,7 @@
|
|
1
|
-
describe
|
1
|
+
describe Berater::Matchers::Overloaded do
|
2
|
+
|
2
3
|
context 'Berater::Unlimiter' do
|
3
|
-
let(:limiter) { Berater.new(:unlimited) }
|
4
|
+
let(:limiter) { Berater.new(:key, :unlimited) }
|
4
5
|
|
5
6
|
it { expect(limiter).not_to be_overloaded }
|
6
7
|
it { expect(limiter).not_to be_inhibited }
|
@@ -19,7 +20,7 @@ describe 'be_overloaded' do
|
|
19
20
|
end
|
20
21
|
|
21
22
|
context 'Berater::Inhibitor' do
|
22
|
-
let(:limiter) { Berater.new(:inhibited) }
|
23
|
+
let(:limiter) { Berater.new(:key, :inhibited) }
|
23
24
|
|
24
25
|
it { expect(limiter).to be_overloaded }
|
25
26
|
it { expect(limiter).to be_inhibited }
|
@@ -32,7 +33,7 @@ describe 'be_overloaded' do
|
|
32
33
|
end
|
33
34
|
|
34
35
|
context 'Berater::RateLimiter' do
|
35
|
-
let(:limiter) { Berater.new(:
|
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(:
|
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,165 +1,228 @@
|
|
1
1
|
describe Berater::RateLimiter do
|
2
|
-
|
2
|
+
it_behaves_like 'a limiter', Berater.new(:key, 3, :second)
|
3
3
|
|
4
4
|
describe '.new' do
|
5
|
-
let(:limiter) { described_class.new(1, :second) }
|
5
|
+
let(:limiter) { described_class.new(:key, 1, :second) }
|
6
6
|
|
7
7
|
it 'initializes' do
|
8
|
-
expect(limiter.
|
9
|
-
expect(limiter.
|
8
|
+
expect(limiter.key).to be :key
|
9
|
+
expect(limiter.capacity).to eq 1
|
10
|
+
expect(limiter.interval).to eq :second
|
10
11
|
end
|
11
12
|
|
12
13
|
it 'has default values' do
|
13
|
-
expect(limiter.key).to eq described_class.to_s
|
14
14
|
expect(limiter.redis).to be Berater.redis
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
describe '#
|
19
|
-
def
|
20
|
-
limiter = described_class.new(
|
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(
|
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(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, 1) }
|
56
|
-
it { expect_interval(:second, 1) }
|
57
|
-
it { expect_interval(:seconds, 1) }
|
43
|
+
# see spec/utils_spec.rb for more
|
58
44
|
|
59
|
-
|
60
|
-
it { expect_interval(:minute, 60) }
|
61
|
-
it { expect_interval(:minutes, 60) }
|
45
|
+
subject { described_class.new(:key, 1, :second) }
|
62
46
|
|
63
|
-
|
64
|
-
|
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
|
65
50
|
end
|
66
51
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
end
|
52
|
+
it 'must be > 0' do
|
53
|
+
expect {
|
54
|
+
described_class.new(:key, 1, 0)
|
55
|
+
}.to raise_error(ArgumentError)
|
72
56
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
described_class.new(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') }
|
57
|
+
expect {
|
58
|
+
described_class.new(:key, 1, -1)
|
59
|
+
}.to raise_error(ArgumentError)
|
83
60
|
end
|
84
61
|
end
|
85
62
|
|
86
63
|
describe '#limit' do
|
87
|
-
let(:limiter) { described_class.new(3, :second) }
|
64
|
+
let(:limiter) { described_class.new(:key, 3, :second) }
|
88
65
|
|
89
66
|
it 'works' do
|
90
|
-
expect(limiter.limit).to eq 1
|
91
|
-
end
|
92
|
-
|
93
|
-
it 'counts' do
|
94
|
-
expect(limiter.limit).to eq 1
|
95
|
-
expect(limiter.limit).to eq 2
|
96
|
-
expect(limiter.limit).to eq 3
|
97
|
-
end
|
98
|
-
|
99
|
-
it 'yields' do
|
100
67
|
expect {|b| limiter.limit(&b) }.to yield_control
|
101
68
|
expect(limiter.limit { 123 }).to eq 123
|
102
69
|
end
|
103
70
|
|
71
|
+
it 'works without a block' do
|
72
|
+
expect(limiter.limit).to be_a Berater::Lock
|
73
|
+
end
|
74
|
+
|
104
75
|
it 'limits excessive calls' do
|
105
76
|
3.times { limiter.limit }
|
106
77
|
|
107
|
-
expect
|
78
|
+
expect(limiter).to be_overrated
|
108
79
|
end
|
109
80
|
|
110
|
-
it 'limit
|
111
|
-
|
112
|
-
expect(limiter.limit).to eq 2
|
113
|
-
expect(limiter.limit).to eq 3
|
81
|
+
it 'resets limit over time' do
|
82
|
+
3.times { limiter.limit }
|
114
83
|
expect(limiter).to be_overrated
|
115
84
|
|
116
|
-
# travel forward a second
|
117
85
|
Timecop.freeze(1)
|
118
86
|
|
119
|
-
|
120
|
-
expect(limiter.limit).to eq 2
|
121
|
-
expect(limiter.limit).to eq 3
|
87
|
+
3.times { limiter.limit }
|
122
88
|
expect(limiter).to be_overrated
|
123
89
|
end
|
124
|
-
end
|
125
90
|
|
126
|
-
|
127
|
-
|
128
|
-
|
91
|
+
context 'with millisecond precision' do
|
92
|
+
it 'resets limit over time' do
|
93
|
+
3.times { limiter.limit }
|
94
|
+
expect(limiter).to be_overrated
|
95
|
+
|
96
|
+
# travel forward to just before the count decrements
|
97
|
+
Timecop.freeze(0.333)
|
98
|
+
expect(limiter).to be_overrated
|
99
|
+
|
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)
|
129
108
|
|
130
|
-
|
131
|
-
|
109
|
+
limiter.capacity.times { limiter.limit }
|
110
|
+
expect(limiter).to be_overrated
|
132
111
|
|
133
|
-
|
134
|
-
|
112
|
+
Timecop.freeze(0.001)
|
113
|
+
expect(limiter).not_to be_overrated
|
114
|
+
|
115
|
+
2.times { limiter.limit }
|
116
|
+
end
|
135
117
|
end
|
136
|
-
end
|
137
118
|
|
138
|
-
|
139
|
-
|
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
|
140
168
|
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
144
181
|
|
145
|
-
|
146
|
-
|
182
|
+
expect(limiter_one).to be_overrated
|
183
|
+
expect(limiter_two.limit).not_to be_overrated
|
184
|
+
|
185
|
+
expect(limiter_one).to be_overrated
|
186
|
+
expect(limiter_two).to be_overrated
|
187
|
+
end
|
147
188
|
end
|
148
189
|
end
|
149
190
|
|
150
|
-
|
151
|
-
let(:
|
152
|
-
let(:limiter_two) { described_class.new(2, :second, key: :two) }
|
191
|
+
describe '#overloaded?' do
|
192
|
+
let(:limiter) { described_class.new(:key, 1, :second) }
|
153
193
|
|
154
|
-
it 'works
|
155
|
-
expect(
|
156
|
-
|
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
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe '#to_s' do
|
204
|
+
def check(capacity, interval, expected)
|
205
|
+
expect(
|
206
|
+
described_class.new(:key, capacity, interval).to_s
|
207
|
+
).to match(expected)
|
208
|
+
end
|
157
209
|
|
158
|
-
|
159
|
-
|
210
|
+
it 'works with symbols' do
|
211
|
+
check(1, :second, /1 per second/)
|
212
|
+
check(1, :minute, /1 per minute/)
|
213
|
+
check(1, :hour, /1 per hour/)
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'works with strings' do
|
217
|
+
check(1, 'second', /1 per second/)
|
218
|
+
check(1, 'minute', /1 per minute/)
|
219
|
+
check(1, 'hour', /1 per hour/)
|
220
|
+
end
|
160
221
|
|
161
|
-
|
162
|
-
|
222
|
+
it 'works with integers' do
|
223
|
+
check(1, 1, /1 every second/)
|
224
|
+
check(1, 2, /1 every 2 seconds/)
|
225
|
+
check(2, 3, /2 every 3 seconds/)
|
163
226
|
end
|
164
227
|
end
|
165
228
|
|