berater 0.6.0 → 0.8.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 +14 -13
- data/lib/berater/concurrency_limiter.rb +19 -37
- data/lib/berater/dsl.rb +8 -8
- data/lib/berater/inhibitor.rb +4 -7
- data/lib/berater/limiter.rb +49 -19
- data/lib/berater/lock.rb +2 -1
- data/lib/berater/rate_limiter.rb +40 -40
- data/lib/berater/rspec.rb +1 -2
- data/lib/berater/rspec/matchers.rb +26 -48
- data/lib/berater/static_limiter.rb +49 -0
- data/lib/berater/test_mode.rb +31 -36
- data/lib/berater/unlimiter.rb +8 -6
- data/lib/berater/utils.rb +9 -0
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +26 -76
- data/spec/concurrency_limiter_spec.rb +73 -57
- data/spec/dsl_refinement_spec.rb +0 -12
- data/spec/dsl_spec.rb +5 -17
- data/spec/inhibitor_spec.rb +10 -5
- data/spec/limiter_spec.rb +95 -9
- data/spec/matchers_spec.rb +21 -85
- data/spec/rate_limiter_spec.rb +88 -38
- data/spec/riddle_spec.rb +6 -2
- data/spec/static_limiter_spec.rb +79 -0
- data/spec/test_mode_spec.rb +48 -106
- data/spec/unlimiter_spec.rb +11 -5
- metadata +5 -2
data/spec/matchers_spec.rb
CHANGED
@@ -1,118 +1,80 @@
|
|
1
1
|
describe Berater::Matchers::Overloaded do
|
2
2
|
|
3
3
|
context 'Berater::Unlimiter' do
|
4
|
-
let(:limiter) { Berater.new
|
4
|
+
let(:limiter) { Berater::Unlimiter.new }
|
5
5
|
|
6
6
|
it { expect(limiter).not_to be_overloaded }
|
7
|
-
it { expect(limiter).not_to be_inhibited }
|
8
|
-
it { expect(limiter).not_to be_overrated }
|
9
|
-
it { expect(limiter).not_to be_incapacitated }
|
10
|
-
|
11
7
|
it { expect { limiter }.not_to be_overloaded }
|
12
|
-
it { expect { limiter }.not_to be_inhibited }
|
13
|
-
it { expect { limiter }.not_to be_overrated }
|
14
|
-
it { expect { limiter }.not_to be_incapacitated }
|
15
|
-
|
16
8
|
it { expect { limiter.limit }.not_to be_overloaded }
|
17
|
-
it { expect { limiter.limit }.not_to be_inhibited }
|
18
|
-
it { expect { limiter.limit }.not_to be_overrated }
|
19
|
-
it { expect { limiter.limit }.not_to be_incapacitated }
|
20
9
|
end
|
21
10
|
|
22
11
|
context 'Berater::Inhibitor' do
|
23
|
-
let(:limiter) { Berater.new
|
12
|
+
let(:limiter) { Berater::Inhibitor.new }
|
24
13
|
|
25
14
|
it { expect(limiter).to be_overloaded }
|
26
|
-
it { expect(limiter).to be_inhibited }
|
27
|
-
|
28
15
|
it { expect { limiter }.to be_overloaded }
|
29
|
-
it { expect { limiter }.to be_inhibited }
|
30
|
-
|
31
16
|
it { expect { limiter.limit }.to be_overloaded }
|
32
|
-
it { expect { limiter.limit }.to be_inhibited }
|
33
17
|
end
|
34
18
|
|
35
19
|
context 'Berater::RateLimiter' do
|
36
|
-
let(:limiter) { Berater.new(:key, 1, :second) }
|
20
|
+
let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
|
37
21
|
|
38
22
|
it { expect(limiter).not_to be_overloaded }
|
39
|
-
it { expect(limiter).not_to be_inhibited }
|
40
|
-
it { expect(limiter).not_to be_overrated }
|
41
|
-
it { expect(limiter).not_to be_incapacitated }
|
42
|
-
|
43
23
|
it { expect { limiter }.not_to be_overloaded }
|
44
|
-
it { expect { limiter }.not_to be_inhibited }
|
45
|
-
it { expect { limiter }.not_to be_overrated }
|
46
|
-
it { expect { limiter }.not_to be_incapacitated }
|
47
|
-
|
48
24
|
it { expect { limiter.limit }.not_to be_overloaded }
|
49
|
-
it { expect { limiter.limit }.not_to be_inhibited }
|
50
|
-
it { expect { limiter.limit }.not_to be_overrated }
|
51
|
-
it { expect { limiter.limit }.not_to be_incapacitated }
|
52
25
|
|
53
26
|
context 'once limit is used up' do
|
54
27
|
before { limiter.limit }
|
55
28
|
|
56
|
-
it 'should
|
57
|
-
expect(limiter).to
|
29
|
+
it 'should be_overloaded' do
|
30
|
+
expect(limiter).to be_overloaded
|
58
31
|
end
|
59
32
|
|
60
|
-
it 'should
|
61
|
-
expect { limiter }.to
|
33
|
+
it 'should be_overloaded' do
|
34
|
+
expect { limiter }.to be_overloaded
|
62
35
|
end
|
63
36
|
|
64
|
-
it 'should
|
65
|
-
expect { limiter.limit }.to
|
37
|
+
it 'should be_overloaded' do
|
38
|
+
expect { limiter.limit }.to be_overloaded
|
66
39
|
end
|
67
40
|
end
|
68
41
|
end
|
69
42
|
|
70
43
|
context 'Berater::ConcurrencyLimiter' do
|
71
|
-
let(:limiter) { Berater.new(:key, 1) }
|
44
|
+
let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
|
72
45
|
|
73
46
|
it { expect(limiter).not_to be_overloaded }
|
74
|
-
it { expect(limiter).not_to be_inhibited }
|
75
|
-
it { expect(limiter).not_to be_overrated }
|
76
|
-
it { expect(limiter).not_to be_incapacitated }
|
77
|
-
|
78
47
|
it { expect { limiter }.not_to be_overloaded }
|
79
|
-
it { expect { limiter }.not_to be_inhibited }
|
80
|
-
it { expect { limiter }.not_to be_overrated }
|
81
|
-
it { expect { limiter }.not_to be_incapacitated }
|
82
|
-
|
83
48
|
it { expect { limiter.limit }.not_to be_overloaded }
|
84
|
-
it { expect { limiter.limit }.not_to be_inhibited }
|
85
|
-
it { expect { limiter.limit }.not_to be_overrated }
|
86
|
-
it { expect { limiter.limit }.not_to be_incapacitated }
|
87
49
|
|
88
50
|
context 'when lock is released' do
|
89
|
-
it 'should
|
51
|
+
it 'should be_overloaded' do
|
90
52
|
3.times do
|
91
|
-
expect(limiter).not_to
|
53
|
+
expect(limiter).not_to be_overloaded
|
92
54
|
end
|
93
55
|
end
|
94
56
|
|
95
|
-
it 'should
|
57
|
+
it 'should be_overloaded' do
|
96
58
|
3.times do
|
97
|
-
expect { limiter }.not_to
|
59
|
+
expect { limiter }.not_to be_overloaded
|
98
60
|
end
|
99
61
|
end
|
100
62
|
|
101
|
-
it 'should
|
63
|
+
it 'should be_overloaded' do
|
102
64
|
3.times do
|
103
|
-
expect { limiter.limit {} }.not_to
|
65
|
+
expect { limiter.limit {} }.not_to be_overloaded
|
104
66
|
end
|
105
67
|
end
|
106
68
|
end
|
107
69
|
|
108
70
|
context 'when lock is *not* released' do
|
109
|
-
it 'should
|
110
|
-
expect { limiter.limit }.not_to
|
111
|
-
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
|
112
74
|
end
|
113
75
|
|
114
|
-
it 'should
|
115
|
-
expect { 3.times { limiter.limit } }.to
|
76
|
+
it 'should be_overloaded' do
|
77
|
+
expect { 3.times { limiter.limit } }.to be_overloaded
|
116
78
|
end
|
117
79
|
end
|
118
80
|
end
|
@@ -156,31 +118,5 @@ describe Berater::Matchers::Overloaded do
|
|
156
118
|
expect { raise Berater::Overloaded }.not_to be_overloaded
|
157
119
|
}.to fail_including("did not expect #{Berater::Overloaded} to be raised")
|
158
120
|
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
121
|
end
|
186
122
|
end
|
data/spec/rate_limiter_spec.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
describe Berater::RateLimiter do
|
2
|
-
it_behaves_like 'a limiter',
|
2
|
+
it_behaves_like 'a limiter', described_class.new(:key, 3, :second)
|
3
|
+
it_behaves_like 'a limiter', described_class.new(:key, 3.5, :second)
|
3
4
|
|
4
5
|
describe '.new' do
|
5
6
|
let(:limiter) { described_class.new(:key, 1, :second) }
|
@@ -36,6 +37,7 @@ describe Berater::RateLimiter do
|
|
36
37
|
it { expect_bad_capacity(-1) }
|
37
38
|
it { expect_bad_capacity('1') }
|
38
39
|
it { expect_bad_capacity(:one) }
|
40
|
+
it { expect_bad_capacity(Float::INFINITY) }
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
@@ -46,7 +48,7 @@ describe Berater::RateLimiter do
|
|
46
48
|
|
47
49
|
it 'saves the interval in original and millisecond format' do
|
48
50
|
expect(subject.interval).to be :second
|
49
|
-
expect(subject.instance_variable_get(:@
|
51
|
+
expect(subject.instance_variable_get(:@interval)).to be 10**3
|
50
52
|
end
|
51
53
|
|
52
54
|
it 'must be > 0' do
|
@@ -75,42 +77,42 @@ describe Berater::RateLimiter do
|
|
75
77
|
it 'limits excessive calls' do
|
76
78
|
3.times { limiter.limit }
|
77
79
|
|
78
|
-
expect(limiter).to
|
80
|
+
expect(limiter).to be_overloaded
|
79
81
|
end
|
80
82
|
|
81
83
|
it 'resets limit over time' do
|
82
84
|
3.times { limiter.limit }
|
83
|
-
expect(limiter).to
|
85
|
+
expect(limiter).to be_overloaded
|
84
86
|
|
85
87
|
Timecop.freeze(1)
|
86
88
|
|
87
89
|
3.times { limiter.limit }
|
88
|
-
expect(limiter).to
|
90
|
+
expect(limiter).to be_overloaded
|
89
91
|
end
|
90
92
|
|
91
93
|
context 'with millisecond precision' do
|
92
94
|
it 'resets limit over time' do
|
93
95
|
3.times { limiter.limit }
|
94
|
-
expect(limiter).to
|
96
|
+
expect(limiter).to be_overloaded
|
95
97
|
|
96
98
|
# travel forward to just before the count decrements
|
97
99
|
Timecop.freeze(0.333)
|
98
|
-
expect(limiter).to
|
100
|
+
expect(limiter).to be_overloaded
|
99
101
|
|
100
102
|
# traveling one more millisecond will decrement the count
|
101
103
|
Timecop.freeze(0.001)
|
102
104
|
limiter.limit
|
103
|
-
expect(limiter).to
|
105
|
+
expect(limiter).to be_overloaded
|
104
106
|
end
|
105
107
|
|
106
108
|
it 'works when drip rate is < 1 per millisecond' do
|
107
109
|
limiter = described_class.new(:key, 2_000, :second)
|
108
110
|
|
109
111
|
limiter.capacity.times { limiter.limit }
|
110
|
-
expect(limiter).to
|
112
|
+
expect(limiter).to be_overloaded
|
111
113
|
|
112
114
|
Timecop.freeze(0.001)
|
113
|
-
expect(limiter).not_to
|
115
|
+
expect(limiter).not_to be_overloaded
|
114
116
|
|
115
117
|
2.times { limiter.limit }
|
116
118
|
end
|
@@ -121,9 +123,9 @@ describe Berater::RateLimiter do
|
|
121
123
|
|
122
124
|
it 'still works' do
|
123
125
|
limiter.limit
|
124
|
-
expect(limiter).not_to
|
126
|
+
expect(limiter).not_to be_overloaded
|
125
127
|
|
126
|
-
expect { limiter.limit }.to
|
128
|
+
expect { limiter.limit }.to be_overloaded
|
127
129
|
|
128
130
|
limiter.limit(cost: 0.5)
|
129
131
|
end
|
@@ -132,30 +134,72 @@ describe Berater::RateLimiter do
|
|
132
134
|
it 'accepts a dynamic capacity' do
|
133
135
|
limiter = described_class.new(:key, 1, :second)
|
134
136
|
|
135
|
-
expect { limiter.limit(capacity: 0) }.to
|
137
|
+
expect { limiter.limit(capacity: 0) }.to be_overloaded
|
136
138
|
5.times { limiter.limit(capacity: 10) }
|
137
|
-
expect { limiter }.to
|
139
|
+
expect { limiter }.to be_overloaded
|
138
140
|
end
|
139
141
|
|
140
142
|
context 'works with cost parameter' do
|
141
|
-
it { expect { limiter.limit(cost: 4) }.to
|
143
|
+
it { expect { limiter.limit(cost: 4) }.to be_overloaded }
|
142
144
|
|
143
145
|
it 'works within limit' do
|
144
146
|
limiter.limit(cost: 3)
|
145
|
-
expect { limiter.limit }.to
|
147
|
+
expect { limiter.limit }.to be_overloaded
|
146
148
|
end
|
147
149
|
|
148
150
|
it 'resets over time' do
|
149
151
|
limiter.limit(cost: 3)
|
150
|
-
expect(limiter).to
|
152
|
+
expect(limiter).to be_overloaded
|
151
153
|
|
152
154
|
Timecop.freeze(1)
|
153
|
-
expect(limiter).not_to
|
155
|
+
expect(limiter).not_to be_overloaded
|
154
156
|
end
|
155
157
|
|
156
|
-
|
157
|
-
|
158
|
-
|
158
|
+
context 'when cost is a Float' do
|
159
|
+
it 'still works' do
|
160
|
+
2.times { limiter.limit(cost: 1.5) }
|
161
|
+
expect(limiter).to be_overloaded
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'calculates contention correctly' do
|
165
|
+
# note: Redis must return Floats as strings to maintain precision
|
166
|
+
lock = limiter.limit(cost: 1.5)
|
167
|
+
expect(lock.contention).to be 1.5
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'with clock skew' do
|
173
|
+
let(:limiter) { described_class.new(:key, 10, :second) }
|
174
|
+
|
175
|
+
it 'works skewing backward' do
|
176
|
+
limiter.limit(cost: 9)
|
177
|
+
|
178
|
+
Timecop.freeze(-0.1) do
|
179
|
+
limiter.limit
|
180
|
+
expect(limiter).to be_overloaded
|
181
|
+
end
|
182
|
+
|
183
|
+
expect(limiter).to be_overloaded
|
184
|
+
|
185
|
+
Timecop.freeze(0.1)
|
186
|
+
limiter.limit
|
187
|
+
expect(limiter).to be_overloaded
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'works skewing forward' do
|
191
|
+
limiter.limit
|
192
|
+
|
193
|
+
Timecop.freeze(0.1) do
|
194
|
+
# one drip later
|
195
|
+
limiter.limit(cost: 10)
|
196
|
+
expect(limiter).to be_overloaded
|
197
|
+
end
|
198
|
+
|
199
|
+
expect(limiter).to be_overloaded
|
200
|
+
|
201
|
+
Timecop.freeze(0.1)
|
202
|
+
expect(limiter).to be_overloaded
|
159
203
|
end
|
160
204
|
end
|
161
205
|
|
@@ -164,10 +208,10 @@ describe Berater::RateLimiter do
|
|
164
208
|
let(:limiter_two) { described_class.new(:key, 1, :second) }
|
165
209
|
|
166
210
|
it 'works as expected' do
|
167
|
-
expect(limiter_one.limit).not_to
|
211
|
+
expect(limiter_one.limit).not_to be_overloaded
|
168
212
|
|
169
|
-
expect(limiter_one).to
|
170
|
-
expect(limiter_two).to
|
213
|
+
expect(limiter_one).to be_overloaded
|
214
|
+
expect(limiter_two).to be_overloaded
|
171
215
|
end
|
172
216
|
end
|
173
217
|
|
@@ -176,27 +220,33 @@ describe Berater::RateLimiter do
|
|
176
220
|
let(:limiter_two) { described_class.new(:two, 2, :second) }
|
177
221
|
|
178
222
|
it 'works as expected' do
|
179
|
-
expect(limiter_one.limit).not_to
|
180
|
-
expect(limiter_two.limit).not_to
|
223
|
+
expect(limiter_one.limit).not_to be_overloaded
|
224
|
+
expect(limiter_two.limit).not_to be_overloaded
|
181
225
|
|
182
|
-
expect(limiter_one).to
|
183
|
-
expect(limiter_two.limit).not_to
|
226
|
+
expect(limiter_one).to be_overloaded
|
227
|
+
expect(limiter_two.limit).not_to be_overloaded
|
184
228
|
|
185
|
-
expect(limiter_one).to
|
186
|
-
expect(limiter_two).to
|
229
|
+
expect(limiter_one).to be_overloaded
|
230
|
+
expect(limiter_two).to be_overloaded
|
187
231
|
end
|
188
232
|
end
|
189
233
|
end
|
190
234
|
|
191
|
-
describe '#
|
192
|
-
let(:limiter) { described_class.new(:key,
|
235
|
+
describe '#utilization' do
|
236
|
+
let(:limiter) { described_class.new(:key, 10, :minute) }
|
193
237
|
|
194
|
-
it
|
195
|
-
expect(limiter.
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
238
|
+
it do
|
239
|
+
expect(limiter.utilization).to be 0.0
|
240
|
+
|
241
|
+
2.times { limiter.limit }
|
242
|
+
expect(limiter.utilization).to be 0.2
|
243
|
+
|
244
|
+
8.times { limiter.limit }
|
245
|
+
expect(limiter.utilization).to be 1.0
|
246
|
+
|
247
|
+
Timecop.freeze(30)
|
248
|
+
|
249
|
+
expect(limiter.utilization).to be 0.5
|
200
250
|
end
|
201
251
|
end
|
202
252
|
|
data/spec/riddle_spec.rb
CHANGED
@@ -42,11 +42,15 @@ class ConcurrenyRiddler
|
|
42
42
|
timeout ||= 1_000 # fake infinity
|
43
43
|
|
44
44
|
limiter = Berater::RateLimiter.new(:key, capacity, timeout)
|
45
|
-
limiter.limit
|
45
|
+
lock = limiter.limit
|
46
46
|
yield if block_given?
|
47
47
|
ensure
|
48
48
|
# decrement counter
|
49
|
-
|
49
|
+
if lock
|
50
|
+
key = limiter.send(:cache_key)
|
51
|
+
count, ts = limiter.redis.get(key).split ';'
|
52
|
+
limiter.redis.set(key, "#{count.to_f - 1};#{ts}")
|
53
|
+
end
|
50
54
|
end
|
51
55
|
end
|
52
56
|
|
@@ -0,0 +1,79 @@
|
|
1
|
+
describe Berater::StaticLimiter do
|
2
|
+
it_behaves_like 'a limiter', described_class.new(:key, 3)
|
3
|
+
it_behaves_like 'a limiter', described_class.new(:key, 3.5)
|
4
|
+
|
5
|
+
describe '#limit' do
|
6
|
+
let(:limiter) { described_class.new(:key, 3) }
|
7
|
+
|
8
|
+
it 'limits excessive calls' do
|
9
|
+
3.times { limiter.limit }
|
10
|
+
|
11
|
+
expect(limiter).to be_overloaded
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'when capacity is a Float' do
|
15
|
+
let(:limiter) { described_class.new(:key, 1.5) }
|
16
|
+
|
17
|
+
it 'still works' do
|
18
|
+
limiter.limit
|
19
|
+
expect(limiter).not_to be_overloaded
|
20
|
+
|
21
|
+
expect { limiter.limit }.to be_overloaded
|
22
|
+
|
23
|
+
limiter.limit(cost: 0.5)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'accepts a dynamic capacity' do
|
28
|
+
limiter = described_class.new(:key, 1)
|
29
|
+
|
30
|
+
expect { limiter.limit(capacity: 0) }.to be_overloaded
|
31
|
+
5.times { limiter.limit(capacity: 10) }
|
32
|
+
expect { limiter }.to be_overloaded
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'works with cost parameter' do
|
36
|
+
let(:limiter) { described_class.new(:key, 3) }
|
37
|
+
|
38
|
+
it { expect { limiter.limit(cost: 4) }.to be_overloaded }
|
39
|
+
|
40
|
+
it 'works within limit' do
|
41
|
+
limiter.limit(cost: 3)
|
42
|
+
expect { limiter.limit }.to be_overloaded
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when cost is a Float' do
|
46
|
+
it 'still works' do
|
47
|
+
2.times { limiter.limit(cost: 1.5) }
|
48
|
+
expect(limiter).to be_overloaded
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'calculates contention correctly' do
|
52
|
+
lock = limiter.limit(cost: 1.5)
|
53
|
+
expect(lock.contention).to be 1.5
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '#utilization' do
|
60
|
+
let(:limiter) { described_class.new(:key, 10) }
|
61
|
+
|
62
|
+
it do
|
63
|
+
expect(limiter.utilization).to be 0.0
|
64
|
+
|
65
|
+
2.times { limiter.limit }
|
66
|
+
expect(limiter.utilization).to be 0.2
|
67
|
+
|
68
|
+
8.times { limiter.limit }
|
69
|
+
expect(limiter.utilization).to be 1.0
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#to_s' do
|
74
|
+
let(:limiter) { described_class.new(:key, 3) }
|
75
|
+
|
76
|
+
it { expect(limiter.to_s).to include '3' }
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|