berater 0.6.2 → 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 +10 -7
- data/lib/berater/concurrency_limiter.rb +8 -11
- data/lib/berater/dsl.rb +8 -8
- data/lib/berater/inhibitor.rb +1 -5
- data/lib/berater/limiter.rb +12 -6
- data/lib/berater/rate_limiter.rb +31 -24
- data/lib/berater/rspec/matchers.rb +11 -31
- data/lib/berater/test_mode.rb +1 -8
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +9 -9
- data/spec/concurrency_limiter_spec.rb +51 -51
- data/spec/dsl_refinement_spec.rb +0 -12
- data/spec/dsl_spec.rb +5 -17
- data/spec/limiter_spec.rb +1 -1
- data/spec/matchers_spec.rb +21 -85
- data/spec/rate_limiter_spec.rb +86 -37
- data/spec/riddle_spec.rb +6 -2
- data/spec/test_mode_spec.rb +19 -102
- metadata +2 -2
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) }
|
@@ -75,42 +76,42 @@ describe Berater::RateLimiter do
|
|
75
76
|
it 'limits excessive calls' do
|
76
77
|
3.times { limiter.limit }
|
77
78
|
|
78
|
-
expect(limiter).to
|
79
|
+
expect(limiter).to be_overloaded
|
79
80
|
end
|
80
81
|
|
81
82
|
it 'resets limit over time' do
|
82
83
|
3.times { limiter.limit }
|
83
|
-
expect(limiter).to
|
84
|
+
expect(limiter).to be_overloaded
|
84
85
|
|
85
86
|
Timecop.freeze(1)
|
86
87
|
|
87
88
|
3.times { limiter.limit }
|
88
|
-
expect(limiter).to
|
89
|
+
expect(limiter).to be_overloaded
|
89
90
|
end
|
90
91
|
|
91
92
|
context 'with millisecond precision' do
|
92
93
|
it 'resets limit over time' do
|
93
94
|
3.times { limiter.limit }
|
94
|
-
expect(limiter).to
|
95
|
+
expect(limiter).to be_overloaded
|
95
96
|
|
96
97
|
# travel forward to just before the count decrements
|
97
98
|
Timecop.freeze(0.333)
|
98
|
-
expect(limiter).to
|
99
|
+
expect(limiter).to be_overloaded
|
99
100
|
|
100
101
|
# traveling one more millisecond will decrement the count
|
101
102
|
Timecop.freeze(0.001)
|
102
103
|
limiter.limit
|
103
|
-
expect(limiter).to
|
104
|
+
expect(limiter).to be_overloaded
|
104
105
|
end
|
105
106
|
|
106
107
|
it 'works when drip rate is < 1 per millisecond' do
|
107
108
|
limiter = described_class.new(:key, 2_000, :second)
|
108
109
|
|
109
110
|
limiter.capacity.times { limiter.limit }
|
110
|
-
expect(limiter).to
|
111
|
+
expect(limiter).to be_overloaded
|
111
112
|
|
112
113
|
Timecop.freeze(0.001)
|
113
|
-
expect(limiter).not_to
|
114
|
+
expect(limiter).not_to be_overloaded
|
114
115
|
|
115
116
|
2.times { limiter.limit }
|
116
117
|
end
|
@@ -121,9 +122,9 @@ describe Berater::RateLimiter do
|
|
121
122
|
|
122
123
|
it 'still works' do
|
123
124
|
limiter.limit
|
124
|
-
expect(limiter).not_to
|
125
|
+
expect(limiter).not_to be_overloaded
|
125
126
|
|
126
|
-
expect { limiter.limit }.to
|
127
|
+
expect { limiter.limit }.to be_overloaded
|
127
128
|
|
128
129
|
limiter.limit(cost: 0.5)
|
129
130
|
end
|
@@ -132,30 +133,72 @@ describe Berater::RateLimiter do
|
|
132
133
|
it 'accepts a dynamic capacity' do
|
133
134
|
limiter = described_class.new(:key, 1, :second)
|
134
135
|
|
135
|
-
expect { limiter.limit(capacity: 0) }.to
|
136
|
+
expect { limiter.limit(capacity: 0) }.to be_overloaded
|
136
137
|
5.times { limiter.limit(capacity: 10) }
|
137
|
-
expect { limiter }.to
|
138
|
+
expect { limiter }.to be_overloaded
|
138
139
|
end
|
139
140
|
|
140
141
|
context 'works with cost parameter' do
|
141
|
-
it { expect { limiter.limit(cost: 4) }.to
|
142
|
+
it { expect { limiter.limit(cost: 4) }.to be_overloaded }
|
142
143
|
|
143
144
|
it 'works within limit' do
|
144
145
|
limiter.limit(cost: 3)
|
145
|
-
expect { limiter.limit }.to
|
146
|
+
expect { limiter.limit }.to be_overloaded
|
146
147
|
end
|
147
148
|
|
148
149
|
it 'resets over time' do
|
149
150
|
limiter.limit(cost: 3)
|
150
|
-
expect(limiter).to
|
151
|
+
expect(limiter).to be_overloaded
|
151
152
|
|
152
153
|
Timecop.freeze(1)
|
153
|
-
expect(limiter).not_to
|
154
|
+
expect(limiter).not_to be_overloaded
|
154
155
|
end
|
155
156
|
|
156
|
-
|
157
|
-
|
158
|
-
|
157
|
+
context 'when cost is a Float' do
|
158
|
+
it 'still works' do
|
159
|
+
2.times { limiter.limit(cost: 1.5) }
|
160
|
+
expect(limiter).to be_overloaded
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'calculates contention correctly' do
|
164
|
+
# note: Redis must return Floats as strings to maintain precision
|
165
|
+
lock = limiter.limit(cost: 1.5)
|
166
|
+
expect(lock.contention).to be 1.5
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context 'with clock skew' do
|
172
|
+
let(:limiter) { described_class.new(:key, 10, :second) }
|
173
|
+
|
174
|
+
it 'works skewing backward' do
|
175
|
+
limiter.limit(cost: 9)
|
176
|
+
|
177
|
+
Timecop.freeze(-0.1) do
|
178
|
+
limiter.limit
|
179
|
+
expect(limiter).to be_overloaded
|
180
|
+
end
|
181
|
+
|
182
|
+
expect(limiter).to be_overloaded
|
183
|
+
|
184
|
+
Timecop.freeze(0.1)
|
185
|
+
limiter.limit
|
186
|
+
expect(limiter).to be_overloaded
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'works skewing forward' do
|
190
|
+
limiter.limit
|
191
|
+
|
192
|
+
Timecop.freeze(0.1) do
|
193
|
+
# one drip later
|
194
|
+
limiter.limit(cost: 10)
|
195
|
+
expect(limiter).to be_overloaded
|
196
|
+
end
|
197
|
+
|
198
|
+
expect(limiter).to be_overloaded
|
199
|
+
|
200
|
+
Timecop.freeze(0.1)
|
201
|
+
expect(limiter).to be_overloaded
|
159
202
|
end
|
160
203
|
end
|
161
204
|
|
@@ -164,10 +207,10 @@ describe Berater::RateLimiter do
|
|
164
207
|
let(:limiter_two) { described_class.new(:key, 1, :second) }
|
165
208
|
|
166
209
|
it 'works as expected' do
|
167
|
-
expect(limiter_one.limit).not_to
|
210
|
+
expect(limiter_one.limit).not_to be_overloaded
|
168
211
|
|
169
|
-
expect(limiter_one).to
|
170
|
-
expect(limiter_two).to
|
212
|
+
expect(limiter_one).to be_overloaded
|
213
|
+
expect(limiter_two).to be_overloaded
|
171
214
|
end
|
172
215
|
end
|
173
216
|
|
@@ -176,27 +219,33 @@ describe Berater::RateLimiter do
|
|
176
219
|
let(:limiter_two) { described_class.new(:two, 2, :second) }
|
177
220
|
|
178
221
|
it 'works as expected' do
|
179
|
-
expect(limiter_one.limit).not_to
|
180
|
-
expect(limiter_two.limit).not_to
|
222
|
+
expect(limiter_one.limit).not_to be_overloaded
|
223
|
+
expect(limiter_two.limit).not_to be_overloaded
|
181
224
|
|
182
|
-
expect(limiter_one).to
|
183
|
-
expect(limiter_two.limit).not_to
|
225
|
+
expect(limiter_one).to be_overloaded
|
226
|
+
expect(limiter_two.limit).not_to be_overloaded
|
184
227
|
|
185
|
-
expect(limiter_one).to
|
186
|
-
expect(limiter_two).to
|
228
|
+
expect(limiter_one).to be_overloaded
|
229
|
+
expect(limiter_two).to be_overloaded
|
187
230
|
end
|
188
231
|
end
|
189
232
|
end
|
190
233
|
|
191
|
-
describe '#
|
192
|
-
let(:limiter) { described_class.new(:key,
|
234
|
+
describe '#utilization' do
|
235
|
+
let(:limiter) { described_class.new(:key, 10, :minute) }
|
193
236
|
|
194
|
-
it
|
195
|
-
expect(limiter.
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
237
|
+
it do
|
238
|
+
expect(limiter.utilization).to be 0.0
|
239
|
+
|
240
|
+
2.times { limiter.limit }
|
241
|
+
expect(limiter.utilization).to be 0.2
|
242
|
+
|
243
|
+
8.times { limiter.limit }
|
244
|
+
expect(limiter.utilization).to be 1.0
|
245
|
+
|
246
|
+
Timecop.freeze(30)
|
247
|
+
|
248
|
+
expect(limiter.utilization).to be 0.5
|
200
249
|
end
|
201
250
|
end
|
202
251
|
|
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, :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
|
|
data/spec/test_mode_spec.rb
CHANGED
@@ -55,26 +55,28 @@ describe Berater::TestMode do
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
shared_examples 'it
|
58
|
+
shared_examples 'it supports test_mode' do
|
59
59
|
before do
|
60
|
+
# without hitting Redis
|
60
61
|
Berater.redis = nil
|
61
62
|
expect_any_instance_of(Berater::LuaScript).not_to receive(:eval)
|
62
63
|
end
|
63
64
|
|
64
|
-
|
65
|
+
context 'with test_mode = :pass' do
|
66
|
+
before { Berater.test_mode = :pass }
|
65
67
|
|
66
|
-
|
67
|
-
10.times { subject.limit }
|
68
|
-
end
|
69
|
-
end
|
68
|
+
it_behaves_like 'it is not overloaded'
|
70
69
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
expect_any_instance_of(Berater::LuaScript).not_to receive(:eval)
|
70
|
+
it 'always works' do
|
71
|
+
10.times { subject.limit }
|
72
|
+
end
|
75
73
|
end
|
76
74
|
|
77
|
-
|
75
|
+
context 'with test_mode = :fail' do
|
76
|
+
before { Berater.test_mode = :fail }
|
77
|
+
|
78
|
+
it_behaves_like 'it is overloaded'
|
79
|
+
end
|
78
80
|
end
|
79
81
|
|
80
82
|
describe 'Unlimiter' do
|
@@ -83,28 +85,10 @@ describe Berater::TestMode do
|
|
83
85
|
context 'when test_mode = nil' do
|
84
86
|
before { Berater.test_mode = nil }
|
85
87
|
|
86
|
-
it
|
87
|
-
it_behaves_like 'it always works, without redis'
|
88
|
-
end
|
89
|
-
|
90
|
-
context 'when test_mode = :pass' do
|
91
|
-
before { Berater.test_mode = :pass }
|
92
|
-
|
93
|
-
it { is_expected.to be_a Berater::Unlimiter }
|
94
|
-
it_behaves_like 'it always works, without redis'
|
88
|
+
it_behaves_like 'it is not overloaded'
|
95
89
|
end
|
96
90
|
|
97
|
-
|
98
|
-
before { Berater.test_mode = :fail }
|
99
|
-
|
100
|
-
it { is_expected.to be_a Berater::Unlimiter }
|
101
|
-
it_behaves_like 'it never works, without redis'
|
102
|
-
|
103
|
-
it 'supports class specific logic' do
|
104
|
-
expect(subject.overloaded?).to be true
|
105
|
-
expect { subject.limit }.to raise_error(Berater::Overloaded)
|
106
|
-
end
|
107
|
-
end
|
91
|
+
it_behaves_like 'it supports test_mode'
|
108
92
|
end
|
109
93
|
|
110
94
|
describe 'Inhibitor' do
|
@@ -113,47 +97,18 @@ describe Berater::TestMode do
|
|
113
97
|
context 'when test_mode = nil' do
|
114
98
|
before { Berater.test_mode = nil }
|
115
99
|
|
116
|
-
it
|
117
|
-
it_behaves_like 'it never works, without redis'
|
100
|
+
it_behaves_like 'it is overloaded'
|
118
101
|
end
|
119
102
|
|
120
|
-
|
121
|
-
before { Berater.test_mode = :pass }
|
122
|
-
|
123
|
-
it { is_expected.to be_a Berater::Inhibitor }
|
124
|
-
it_behaves_like 'it always works, without redis'
|
125
|
-
end
|
126
|
-
|
127
|
-
context 'when test_mode = :fail' do
|
128
|
-
before { Berater.test_mode = :fail }
|
129
|
-
|
130
|
-
it { is_expected.to be_a Berater::Inhibitor }
|
131
|
-
it_behaves_like 'it never works, without redis'
|
132
|
-
|
133
|
-
it 'supports class specific logic' do
|
134
|
-
expect(subject.inhibited?).to be true
|
135
|
-
expect { subject.limit }.to raise_error(Berater::Inhibitor::Inhibited)
|
136
|
-
end
|
137
|
-
end
|
103
|
+
it_behaves_like 'it supports test_mode'
|
138
104
|
end
|
139
105
|
|
140
106
|
describe 'RateLimiter' do
|
141
107
|
subject { Berater::RateLimiter.new(:key, 1, :second) }
|
142
108
|
|
143
|
-
shared_examples 'a RateLimiter' do
|
144
|
-
it { is_expected.to be_a Berater::RateLimiter }
|
145
|
-
|
146
|
-
it 'checks arguments' do
|
147
|
-
expect {
|
148
|
-
Berater::RateLimiter.new(:key, 1)
|
149
|
-
}.to raise_error(ArgumentError)
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
109
|
context 'when test_mode = nil' do
|
154
110
|
before { Berater.test_mode = nil }
|
155
111
|
|
156
|
-
it_behaves_like 'a RateLimiter'
|
157
112
|
it_behaves_like 'it is not overloaded'
|
158
113
|
|
159
114
|
it 'works per usual' do
|
@@ -163,24 +118,7 @@ describe Berater::TestMode do
|
|
163
118
|
end
|
164
119
|
end
|
165
120
|
|
166
|
-
|
167
|
-
before { Berater.test_mode = :pass }
|
168
|
-
|
169
|
-
it_behaves_like 'a RateLimiter'
|
170
|
-
it_behaves_like 'it always works, without redis'
|
171
|
-
end
|
172
|
-
|
173
|
-
context 'when test_mode = :fail' do
|
174
|
-
before { Berater.test_mode = :fail }
|
175
|
-
|
176
|
-
it_behaves_like 'a RateLimiter'
|
177
|
-
it_behaves_like 'it never works, without redis'
|
178
|
-
|
179
|
-
it 'supports class specific logic' do
|
180
|
-
expect(subject.overrated?).to be true
|
181
|
-
expect { subject.limit }.to raise_error(Berater::RateLimiter::Overrated)
|
182
|
-
end
|
183
|
-
end
|
121
|
+
it_behaves_like 'it supports test_mode'
|
184
122
|
end
|
185
123
|
|
186
124
|
describe 'ConcurrencyLimiter' do
|
@@ -189,8 +127,6 @@ describe Berater::TestMode do
|
|
189
127
|
context 'when test_mode = nil' do
|
190
128
|
before { Berater.test_mode = nil }
|
191
129
|
|
192
|
-
it { is_expected.to be_a Berater::ConcurrencyLimiter }
|
193
|
-
|
194
130
|
it_behaves_like 'it is not overloaded'
|
195
131
|
|
196
132
|
it 'works per usual' do
|
@@ -200,26 +136,7 @@ describe Berater::TestMode do
|
|
200
136
|
end
|
201
137
|
end
|
202
138
|
|
203
|
-
|
204
|
-
before { Berater.test_mode = :pass }
|
205
|
-
|
206
|
-
it { is_expected.to be_a Berater::ConcurrencyLimiter }
|
207
|
-
|
208
|
-
it_behaves_like 'it always works, without redis'
|
209
|
-
end
|
210
|
-
|
211
|
-
context 'when test_mode = :fail' do
|
212
|
-
before { Berater.test_mode = :fail }
|
213
|
-
|
214
|
-
it { is_expected.to be_a Berater::ConcurrencyLimiter }
|
215
|
-
|
216
|
-
it_behaves_like 'it never works, without redis'
|
217
|
-
|
218
|
-
it 'supports class specific logic' do
|
219
|
-
expect(subject.incapacitated?).to be true
|
220
|
-
expect { subject.limit }.to raise_error(Berater::ConcurrencyLimiter::Incapacitated)
|
221
|
-
end
|
222
|
-
end
|
139
|
+
it_behaves_like 'it supports test_mode'
|
223
140
|
end
|
224
141
|
|
225
142
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: berater
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Pepper
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|