berater 0.0.1 → 0.1.4
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 +5 -5
- data/lib/berater.rb +50 -16
- data/lib/berater/base_limiter.rb +32 -0
- data/lib/berater/concurrency_limiter.rb +154 -0
- data/lib/berater/inhibitor.rb +21 -0
- data/lib/berater/rate_limiter.rb +84 -0
- data/lib/berater/unlimiter.rb +19 -0
- data/lib/berater/version.rb +3 -0
- data/spec/berater_spec.rb +192 -0
- data/spec/concurrency_limiter_spec.rb +202 -0
- data/spec/concurrency_lock_spec.rb +92 -0
- data/spec/inhibitor_spec.rb +37 -0
- data/spec/matcher_spec.rb +118 -0
- data/spec/rate_limiter_spec.rb +166 -0
- data/spec/unlimiter_spec.rb +61 -0
- metadata +81 -20
@@ -0,0 +1,202 @@
|
|
1
|
+
describe Berater::ConcurrencyLimiter do
|
2
|
+
before { Berater.mode = :concurrency }
|
3
|
+
|
4
|
+
describe '.new' do
|
5
|
+
let(:limiter) { described_class.new(1) }
|
6
|
+
|
7
|
+
it 'initializes' do
|
8
|
+
expect(limiter.capacity).to be 1
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'has default values' do
|
12
|
+
expect(limiter.key).to eq described_class.to_s
|
13
|
+
expect(limiter.redis).to be Berater.redis
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#capacity' do
|
18
|
+
def expect_capacity(capacity)
|
19
|
+
limiter = described_class.new(capacity)
|
20
|
+
expect(limiter.capacity).to eq capacity
|
21
|
+
end
|
22
|
+
|
23
|
+
it { expect_capacity(0) }
|
24
|
+
it { expect_capacity(1) }
|
25
|
+
it { expect_capacity(10_000) }
|
26
|
+
|
27
|
+
context 'with erroneous values' do
|
28
|
+
def expect_bad_capacity(capacity)
|
29
|
+
expect do
|
30
|
+
described_class.new(capacity)
|
31
|
+
end.to raise_error ArgumentError
|
32
|
+
end
|
33
|
+
|
34
|
+
it { expect_bad_capacity(0.5) }
|
35
|
+
it { expect_bad_capacity(-1) }
|
36
|
+
it { expect_bad_capacity('1') }
|
37
|
+
it { expect_bad_capacity(:one) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#timeout' do
|
42
|
+
def expect_timeout(timeout)
|
43
|
+
limiter = described_class.new(1, timeout: timeout)
|
44
|
+
expect(limiter.timeout).to eq timeout
|
45
|
+
end
|
46
|
+
|
47
|
+
it { expect_timeout(0) }
|
48
|
+
it { expect_timeout(1) }
|
49
|
+
it { expect_timeout(10_000) }
|
50
|
+
|
51
|
+
context 'with erroneous values' do
|
52
|
+
def expect_bad_timeout(timeout)
|
53
|
+
expect do
|
54
|
+
described_class.new(1, timeout: timeout)
|
55
|
+
end.to raise_error ArgumentError
|
56
|
+
end
|
57
|
+
|
58
|
+
it { expect_bad_timeout(0.5) }
|
59
|
+
it { expect_bad_timeout(-1) }
|
60
|
+
it { expect_bad_timeout('1') }
|
61
|
+
it { expect_bad_timeout(:one) }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe '#limit' do
|
66
|
+
let(:limiter) { described_class.new(2, timeout: 1) }
|
67
|
+
|
68
|
+
it 'works' do
|
69
|
+
expect {|b| limiter.limit(&b) }.to yield_control
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'works many times if workers release locks' do
|
73
|
+
30.times do
|
74
|
+
expect {|b| limiter.limit(&b) }.to yield_control
|
75
|
+
end
|
76
|
+
|
77
|
+
30.times do
|
78
|
+
lock = limiter.limit
|
79
|
+
lock.release
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'limits excessive calls' do
|
84
|
+
expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
|
85
|
+
expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
|
86
|
+
|
87
|
+
expect(limiter).to be_incapacitated
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'times out locks' do
|
91
|
+
expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
|
92
|
+
expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
|
93
|
+
expect(limiter).to be_incapacitated
|
94
|
+
|
95
|
+
Timecop.travel(1)
|
96
|
+
|
97
|
+
expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
|
98
|
+
expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
|
99
|
+
expect(limiter).to be_incapacitated
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'with capacity 0' do
|
103
|
+
let(:limiter) { described_class.new(0) }
|
104
|
+
|
105
|
+
it 'always fails' do
|
106
|
+
expect(limiter).to be_incapacitated
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'with same key, different limiters' do
|
112
|
+
let(:limiter_one) { described_class.new(1) }
|
113
|
+
let(:limiter_two) { described_class.new(1) }
|
114
|
+
|
115
|
+
it { expect(limiter_one.key).to eq limiter_two.key }
|
116
|
+
|
117
|
+
it 'works as expected' do
|
118
|
+
expect(limiter_one.limit).to be_a Berater::ConcurrencyLimiter::Lock
|
119
|
+
|
120
|
+
expect(limiter_one).to be_incapacitated
|
121
|
+
expect(limiter_two).to be_incapacitated
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'with different keys, same limiter' do
|
126
|
+
let(:limiter) { described_class.new(1) }
|
127
|
+
|
128
|
+
it 'works as expected' do
|
129
|
+
one_lock = limiter.limit(key: :one)
|
130
|
+
expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
|
131
|
+
|
132
|
+
expect { limiter.limit(key: :one) {} }.to be_incapacitated
|
133
|
+
expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
|
134
|
+
|
135
|
+
two_lock = limiter.limit(key: :two)
|
136
|
+
expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
|
137
|
+
|
138
|
+
expect { limiter.limit(key: :one) {} }.to be_incapacitated
|
139
|
+
expect { limiter.limit(key: :two) {} }.to be_incapacitated
|
140
|
+
|
141
|
+
one_lock.release
|
142
|
+
expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
|
143
|
+
expect { limiter.limit(key: :two) {} }.to be_incapacitated
|
144
|
+
|
145
|
+
two_lock.release
|
146
|
+
expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
|
147
|
+
expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context 'with same key, different capacities' do
|
152
|
+
let(:limiter_one) { described_class.new(1) }
|
153
|
+
let(:limiter_two) { described_class.new(2) }
|
154
|
+
|
155
|
+
it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
|
156
|
+
|
157
|
+
it 'works as expected' do
|
158
|
+
one_lock = limiter_one.limit
|
159
|
+
expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
|
160
|
+
|
161
|
+
expect(limiter_one).to be_incapacitated
|
162
|
+
expect(limiter_two).not_to be_incapacitated
|
163
|
+
|
164
|
+
two_lock = limiter_two.limit
|
165
|
+
expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
|
166
|
+
|
167
|
+
expect(limiter_one).to be_incapacitated
|
168
|
+
expect(limiter_two).to be_incapacitated
|
169
|
+
|
170
|
+
one_lock.release
|
171
|
+
expect(limiter_one).to be_incapacitated
|
172
|
+
expect(limiter_two).not_to be_incapacitated
|
173
|
+
|
174
|
+
two_lock.release
|
175
|
+
expect(limiter_one).not_to be_incapacitated
|
176
|
+
expect(limiter_two).not_to be_incapacitated
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context 'with different keys, different limiters' do
|
181
|
+
let(:limiter_one) { described_class.new(1, key: :one) }
|
182
|
+
let(:limiter_two) { described_class.new(1, key: :two) }
|
183
|
+
|
184
|
+
it 'works as expected' do
|
185
|
+
expect(limiter_one).not_to be_incapacitated
|
186
|
+
expect(limiter_two).not_to be_incapacitated
|
187
|
+
|
188
|
+
one_lock = limiter_one.limit
|
189
|
+
expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
|
190
|
+
|
191
|
+
expect(limiter_one).to be_incapacitated
|
192
|
+
expect(limiter_two).not_to be_incapacitated
|
193
|
+
|
194
|
+
two_lock = limiter_two.limit
|
195
|
+
expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
|
196
|
+
|
197
|
+
expect(limiter_one).to be_incapacitated
|
198
|
+
expect(limiter_two).to be_incapacitated
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
describe Berater::ConcurrencyLimiter::Lock do
|
2
|
+
before { Berater.mode = :concurrency }
|
3
|
+
|
4
|
+
let(:limiter) { Berater.new(:concurrency, 3) }
|
5
|
+
|
6
|
+
describe '#contention' do
|
7
|
+
it 'tracks contention' do
|
8
|
+
lock_1 = limiter.limit
|
9
|
+
expect(lock_1.contention).to eq 1
|
10
|
+
|
11
|
+
lock_2 = limiter.limit
|
12
|
+
expect(lock_2.contention).to eq 2
|
13
|
+
|
14
|
+
limiter.limit do |lock_3|
|
15
|
+
expect(lock_3.contention).to eq 3
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'works in block mode' do
|
20
|
+
lock_1 = limiter.limit
|
21
|
+
|
22
|
+
limiter.limit do |lock_2|
|
23
|
+
expect(lock_1.contention).to eq 1
|
24
|
+
expect(lock_2.contention).to eq 2
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#release' do
|
30
|
+
it 'can not be released twice' do
|
31
|
+
lock = limiter.limit
|
32
|
+
expect(lock.release).to be true
|
33
|
+
expect { lock.release }.to raise_error(RuntimeError, /already/)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'does not work in block mode' do
|
37
|
+
expect do
|
38
|
+
limiter.limit do |lock|
|
39
|
+
lock.release
|
40
|
+
end
|
41
|
+
end.to raise_error(RuntimeError, /already/)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#released?' do
|
46
|
+
it 'works' do
|
47
|
+
lock = limiter.limit
|
48
|
+
expect(lock.released?).to be false
|
49
|
+
|
50
|
+
lock.release
|
51
|
+
expect(lock.released?).to be true
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'works in block mode' do
|
55
|
+
limiter.limit do |lock|
|
56
|
+
expect(lock.released?).to be false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#expired?' do
|
62
|
+
let!(:lock) { limiter.limit }
|
63
|
+
|
64
|
+
context 'when timeout is not set' do
|
65
|
+
it { expect(limiter.timeout).to eq 0 }
|
66
|
+
|
67
|
+
it 'never expires' do
|
68
|
+
expect(lock.expired?).to be false
|
69
|
+
|
70
|
+
Timecop.travel(1_000)
|
71
|
+
|
72
|
+
expect(lock.expired?).to be false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'when timeout is set and exceeded' do
|
77
|
+
before { Timecop.travel(1) }
|
78
|
+
|
79
|
+
let(:limiter) { Berater.new(:concurrency, 3, timeout: 1) }
|
80
|
+
|
81
|
+
it 'expires' do
|
82
|
+
expect(lock.expired?).to be true
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'fails to release' do
|
86
|
+
expect(lock.released?).to be false
|
87
|
+
expect { lock.release }.to raise_error(RuntimeError, /expired/)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
describe Berater::Inhibitor do
|
2
|
+
before { Berater.mode = :inhibited }
|
3
|
+
|
4
|
+
describe '.new' do
|
5
|
+
it 'initializes without any arguments or options' do
|
6
|
+
expect(described_class.new).to be_a described_class
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'initializes with any arguments and options' do
|
10
|
+
expect(described_class.new(:abc, x: 123)).to be_a described_class
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'has default values' do
|
14
|
+
expect(described_class.new.key).to eq described_class.to_s
|
15
|
+
expect(described_class.new.redis).to be Berater.redis
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
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
|
+
describe '#limit' do
|
30
|
+
let(:limiter) { described_class.new }
|
31
|
+
|
32
|
+
it 'always limits' do
|
33
|
+
expect { described_class.limit }.to be_inhibited
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
describe 'be_overloaded' do
|
2
|
+
context 'Berater::Unlimiter' do
|
3
|
+
let(:limiter) { Berater.new(:unlimited) }
|
4
|
+
|
5
|
+
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
|
+
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
|
+
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
|
+
end
|
20
|
+
|
21
|
+
context 'Berater::Inhibitor' do
|
22
|
+
let(:limiter) { Berater.new(:inhibited) }
|
23
|
+
|
24
|
+
it { expect(limiter).to be_overloaded }
|
25
|
+
it { expect(limiter).to be_inhibited }
|
26
|
+
|
27
|
+
it { expect { limiter }.to be_overloaded }
|
28
|
+
it { expect { limiter }.to be_inhibited }
|
29
|
+
|
30
|
+
it { expect { limiter.limit }.to be_overloaded }
|
31
|
+
it { expect { limiter.limit }.to be_inhibited }
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'Berater::RateLimiter' do
|
35
|
+
let(:limiter) { Berater.new(:rate, 1, :second) }
|
36
|
+
|
37
|
+
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
|
+
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
|
+
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
|
+
|
52
|
+
context 'once limit is used up' do
|
53
|
+
before { limiter.limit }
|
54
|
+
|
55
|
+
it 'should be_overrated' do
|
56
|
+
expect(limiter).to be_overrated
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should be_overrated' do
|
60
|
+
expect { limiter }.to be_overrated
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should be_overrated' do
|
64
|
+
expect { limiter.limit }.to be_overrated
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'Berater::ConcurrencyLimiter' do
|
70
|
+
let(:limiter) { Berater.new(:concurrency, 1) }
|
71
|
+
|
72
|
+
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
|
+
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
|
+
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
|
+
|
87
|
+
context 'when lock is released' do
|
88
|
+
it 'should be_incapacitated' do
|
89
|
+
3.times do
|
90
|
+
expect(limiter).not_to be_incapacitated
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should be_incapacitated' do
|
95
|
+
3.times do
|
96
|
+
expect { limiter }.not_to be_incapacitated
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should be_incapacitated' do
|
101
|
+
3.times do
|
102
|
+
expect { limiter.limit {} }.not_to be_incapacitated
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'when lock is *not* released' do
|
108
|
+
it 'should be_incapacitated' do
|
109
|
+
expect { limiter.limit }.not_to be_incapacitated
|
110
|
+
expect { limiter.limit }.to be_incapacitated
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should be_incapacitated' do
|
114
|
+
expect { 3.times { limiter.limit } }.to be_incapacitated
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|