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
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'berater/dsl'
|
2
|
+
|
3
|
+
describe Berater do
|
4
|
+
using Berater::DSL
|
5
|
+
|
6
|
+
it 'instatiates a RateLimiter' do
|
7
|
+
limiter = Berater.new(:key) { 1.per second }
|
8
|
+
expect(limiter).to be_a Berater::RateLimiter
|
9
|
+
expect(limiter.key).to be :key
|
10
|
+
expect(limiter.capacity).to be 1
|
11
|
+
expect(limiter.interval).to be :second
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'instatiates a ConcurrencyLimiter' do
|
15
|
+
limiter = Berater.new(:key, timeout: 2) { 1.at_once }
|
16
|
+
expect(limiter).to be_a Berater::ConcurrencyLimiter
|
17
|
+
expect(limiter.key).to be :key
|
18
|
+
expect(limiter.capacity).to be 1
|
19
|
+
expect(limiter.timeout).to be 2
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'does not accept args and dsl block' do
|
23
|
+
expect {
|
24
|
+
Berater.new(:key, 2) { 3.at_once }
|
25
|
+
}.to raise_error(ArgumentError)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'requires either mode or dsl block' do
|
29
|
+
expect {
|
30
|
+
Berater.new(:key)
|
31
|
+
}.to raise_error(ArgumentError)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/spec/dsl_spec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
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, interval: :second ]) { 1.per second }
|
17
|
+
check([ 3, interval: :minute ]) { 3.per minute }
|
18
|
+
check([ 5, interval: :hour ]) { 5.every hour }
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'cleans up afterward' do
|
22
|
+
check([ 1, interval: :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: 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
|
+
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.utilization }.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,118 +1,122 @@
|
|
1
|
-
describe
|
1
|
+
describe Berater::Matchers::Overloaded do
|
2
|
+
|
2
3
|
context 'Berater::Unlimiter' do
|
3
|
-
let(:limiter) { Berater.new
|
4
|
+
let(:limiter) { Berater::Unlimiter.new }
|
4
5
|
|
5
6
|
it { expect(limiter).not_to be_overloaded }
|
6
|
-
it { expect(limiter).not_to be_inhibited }
|
7
|
-
it { expect(limiter).not_to be_overrated }
|
8
|
-
it { expect(limiter).not_to be_incapacitated }
|
9
|
-
|
10
7
|
it { expect { limiter }.not_to be_overloaded }
|
11
|
-
it { expect { limiter }.not_to be_inhibited }
|
12
|
-
it { expect { limiter }.not_to be_overrated }
|
13
|
-
it { expect { limiter }.not_to be_incapacitated }
|
14
|
-
|
15
8
|
it { expect { limiter.limit }.not_to be_overloaded }
|
16
|
-
it { expect { limiter.limit }.not_to be_inhibited }
|
17
|
-
it { expect { limiter.limit }.not_to be_overrated }
|
18
|
-
it { expect { limiter.limit }.not_to be_incapacitated }
|
19
9
|
end
|
20
10
|
|
21
11
|
context 'Berater::Inhibitor' do
|
22
|
-
let(:limiter) { Berater.new
|
12
|
+
let(:limiter) { Berater::Inhibitor.new }
|
23
13
|
|
24
14
|
it { expect(limiter).to be_overloaded }
|
25
|
-
it { expect(limiter).to be_inhibited }
|
26
|
-
|
27
15
|
it { expect { limiter }.to be_overloaded }
|
28
|
-
it { expect { limiter }.to be_inhibited }
|
29
|
-
|
30
16
|
it { expect { limiter.limit }.to be_overloaded }
|
31
|
-
it { expect { limiter.limit }.to be_inhibited }
|
32
17
|
end
|
33
18
|
|
34
19
|
context 'Berater::RateLimiter' do
|
35
|
-
let(:limiter) { Berater.new(:key,
|
20
|
+
let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
|
36
21
|
|
37
22
|
it { expect(limiter).not_to be_overloaded }
|
38
|
-
it { expect(limiter).not_to be_inhibited }
|
39
|
-
it { expect(limiter).not_to be_overrated }
|
40
|
-
it { expect(limiter).not_to be_incapacitated }
|
41
|
-
|
42
23
|
it { expect { limiter }.not_to be_overloaded }
|
43
|
-
it { expect { limiter }.not_to be_inhibited }
|
44
|
-
it { expect { limiter }.not_to be_overrated }
|
45
|
-
it { expect { limiter }.not_to be_incapacitated }
|
46
|
-
|
47
24
|
it { expect { limiter.limit }.not_to be_overloaded }
|
48
|
-
it { expect { limiter.limit }.not_to be_inhibited }
|
49
|
-
it { expect { limiter.limit }.not_to be_overrated }
|
50
|
-
it { expect { limiter.limit }.not_to be_incapacitated }
|
51
25
|
|
52
26
|
context 'once limit is used up' do
|
53
27
|
before { limiter.limit }
|
54
28
|
|
55
|
-
it 'should
|
56
|
-
expect(limiter).to
|
29
|
+
it 'should be_overloaded' do
|
30
|
+
expect(limiter).to be_overloaded
|
57
31
|
end
|
58
32
|
|
59
|
-
it 'should
|
60
|
-
expect { limiter }.to
|
33
|
+
it 'should be_overloaded' do
|
34
|
+
expect { limiter }.to be_overloaded
|
61
35
|
end
|
62
36
|
|
63
|
-
it 'should
|
64
|
-
expect { limiter.limit }.to
|
37
|
+
it 'should be_overloaded' do
|
38
|
+
expect { limiter.limit }.to be_overloaded
|
65
39
|
end
|
66
40
|
end
|
67
41
|
end
|
68
42
|
|
69
43
|
context 'Berater::ConcurrencyLimiter' do
|
70
|
-
let(:limiter) { Berater.new(:key,
|
44
|
+
let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
|
71
45
|
|
72
46
|
it { expect(limiter).not_to be_overloaded }
|
73
|
-
it { expect(limiter).not_to be_inhibited }
|
74
|
-
it { expect(limiter).not_to be_overrated }
|
75
|
-
it { expect(limiter).not_to be_incapacitated }
|
76
|
-
|
77
47
|
it { expect { limiter }.not_to be_overloaded }
|
78
|
-
it { expect { limiter }.not_to be_inhibited }
|
79
|
-
it { expect { limiter }.not_to be_overrated }
|
80
|
-
it { expect { limiter }.not_to be_incapacitated }
|
81
|
-
|
82
48
|
it { expect { limiter.limit }.not_to be_overloaded }
|
83
|
-
it { expect { limiter.limit }.not_to be_inhibited }
|
84
|
-
it { expect { limiter.limit }.not_to be_overrated }
|
85
|
-
it { expect { limiter.limit }.not_to be_incapacitated }
|
86
49
|
|
87
50
|
context 'when lock is released' do
|
88
|
-
it 'should
|
51
|
+
it 'should be_overloaded' do
|
89
52
|
3.times do
|
90
|
-
expect(limiter).not_to
|
53
|
+
expect(limiter).not_to be_overloaded
|
91
54
|
end
|
92
55
|
end
|
93
56
|
|
94
|
-
it 'should
|
57
|
+
it 'should be_overloaded' do
|
95
58
|
3.times do
|
96
|
-
expect { limiter }.not_to
|
59
|
+
expect { limiter }.not_to be_overloaded
|
97
60
|
end
|
98
61
|
end
|
99
62
|
|
100
|
-
it 'should
|
63
|
+
it 'should be_overloaded' do
|
101
64
|
3.times do
|
102
|
-
expect { limiter.limit {} }.not_to
|
65
|
+
expect { limiter.limit {} }.not_to be_overloaded
|
103
66
|
end
|
104
67
|
end
|
105
68
|
end
|
106
69
|
|
107
70
|
context 'when lock is *not* released' do
|
108
|
-
it 'should
|
109
|
-
expect { limiter.limit }.not_to
|
110
|
-
expect { limiter.limit }.to
|
71
|
+
it 'should be_overloaded' do
|
72
|
+
expect { limiter.limit }.not_to be_overloaded
|
73
|
+
expect { limiter.limit }.to be_overloaded
|
111
74
|
end
|
112
75
|
|
113
|
-
it 'should
|
114
|
-
expect { 3.times { limiter.limit } }.to
|
76
|
+
it 'should be_overloaded' do
|
77
|
+
expect { 3.times { limiter.limit } }.to be_overloaded
|
115
78
|
end
|
116
79
|
end
|
117
80
|
end
|
81
|
+
|
82
|
+
context 'when matchers fail' do
|
83
|
+
let(:unlimiter) { Berater::Unlimiter.new }
|
84
|
+
let(:inhibitor) { Berater::Inhibitor.new }
|
85
|
+
|
86
|
+
it 'catches false negatives' do
|
87
|
+
expect {
|
88
|
+
expect(unlimiter).to be_overloaded
|
89
|
+
}.to fail_including('expected to be overloaded')
|
90
|
+
|
91
|
+
expect {
|
92
|
+
expect { unlimiter }.to be_overloaded
|
93
|
+
}.to fail_including('expected to be overloaded')
|
94
|
+
|
95
|
+
expect {
|
96
|
+
expect { unlimiter.limit }.to be_overloaded
|
97
|
+
}.to fail_including("expected #{Berater::Overloaded} to be raised")
|
98
|
+
|
99
|
+
expect {
|
100
|
+
expect { 123 }.to be_overloaded
|
101
|
+
}.to fail_including("expected #{Berater::Overloaded} to be raised")
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'catches false positives' do
|
105
|
+
expect {
|
106
|
+
expect(inhibitor).not_to be_overloaded
|
107
|
+
}.to fail_including('expected not to be overloaded')
|
108
|
+
|
109
|
+
expect {
|
110
|
+
expect { inhibitor }.not_to be_overloaded
|
111
|
+
}.to fail_including('expected not to be overloaded')
|
112
|
+
|
113
|
+
expect {
|
114
|
+
expect { inhibitor.limit }.not_to be_overloaded
|
115
|
+
}.to fail_including("did not expect #{Berater::Overloaded} to be raised")
|
116
|
+
|
117
|
+
expect {
|
118
|
+
expect { raise Berater::Overloaded }.not_to be_overloaded
|
119
|
+
}.to fail_including("did not expect #{Berater::Overloaded} to be raised")
|
120
|
+
end
|
121
|
+
end
|
118
122
|
end
|