berater 0.6.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,110 @@
1
+ class Meddler
2
+ def call(*)
3
+ yield
4
+ end
5
+ end
6
+
7
+ describe 'Berater.middleware' do
8
+ subject { Berater.middleware }
9
+
10
+ describe 'adding middleware' do
11
+ after { is_expected.to include Meddler }
12
+
13
+ it 'can be done inline' do
14
+ Berater.middleware.use Meddler
15
+ end
16
+
17
+ it 'can be done with a block' do
18
+ Berater.middleware do
19
+ use Meddler
20
+ end
21
+ end
22
+ end
23
+
24
+ it 'resets along with Berater' do
25
+ Berater.middleware.use Meddler
26
+ is_expected.to include Meddler
27
+
28
+ Berater.reset
29
+ is_expected.to be_empty
30
+ end
31
+
32
+ describe 'Berater::Limiter#limit' do
33
+ let(:middleware) { Meddler.new }
34
+ let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
35
+
36
+ before do
37
+ expect(Meddler).to receive(:new).and_return(middleware).at_least(1)
38
+ Berater.middleware.use Meddler
39
+ end
40
+
41
+ it 'calls the middleware' do
42
+ expect(middleware).to receive(:call)
43
+ limiter.limit
44
+ end
45
+
46
+ it 'calls the middleware, passing the limiter and options' do
47
+ expect(middleware).to receive(:call).with(
48
+ limiter,
49
+ hash_including(:capacity, :cost)
50
+ )
51
+
52
+ limiter.limit
53
+ end
54
+
55
+ context 'when used per ususual' do
56
+ before do
57
+ expect(middleware).to receive(:call).and_call_original.at_least(1)
58
+ end
59
+
60
+ it 'still works inline' do
61
+ expect(limiter.limit).to be_a Berater::Lock
62
+ end
63
+
64
+ it 'still works in block mode' do
65
+ expect(limiter.limit { 123 }).to be 123
66
+ end
67
+
68
+ it 'still has limits' do
69
+ limiter.limit
70
+ expect(limiter).to be_overloaded
71
+ end
72
+ end
73
+
74
+ context 'when middleware meddles' do
75
+ it 'can change the capacity' do
76
+ expect(middleware).to receive(:call) do |limiter, opts, &block|
77
+ opts[:capacity] = 0
78
+ block.call
79
+ end
80
+
81
+ expect { limiter.limit }.to be_overloaded
82
+ end
83
+
84
+ it 'can change the cost' do
85
+ expect(middleware).to receive(:call) do |limiter, opts, &block|
86
+ opts[:cost] = 2
87
+ block.call
88
+ end
89
+
90
+ expect { limiter.limit }.to be_overloaded
91
+ end
92
+
93
+ it 'can change the limiter' do
94
+ other_limiter = Berater::Inhibitor.new
95
+
96
+ expect(middleware).to receive(:call) do |limiter, opts, &block|
97
+ block.call other_limiter, opts
98
+ end
99
+ expect(other_limiter).to receive(:acquire_lock).and_call_original
100
+
101
+ expect { limiter.limit }.to be_overloaded
102
+ end
103
+
104
+ it 'can abort by not yielding' do
105
+ expect(middleware).to receive(:call)
106
+ expect(limiter.limit).to be nil
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,5 +1,6 @@
1
1
  describe Berater::RateLimiter do
2
- it_behaves_like 'a limiter', Berater.new(:key, 3, :second)
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(:@interval_msec)).to be 10**3
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 be_overrated
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 be_overrated
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 be_overrated
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 be_overrated
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 be_overrated
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 be_overrated
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 be_overrated
112
+ expect(limiter).to be_overloaded
111
113
 
112
114
  Timecop.freeze(0.001)
113
- expect(limiter).not_to be_overrated
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 be_overrated
126
+ expect(limiter).not_to be_overloaded
125
127
 
126
- expect { limiter.limit }.to be_overrated
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 be_overrated
137
+ expect { limiter.limit(capacity: 0) }.to be_overloaded
136
138
  5.times { limiter.limit(capacity: 10) }
137
- expect { limiter }.to be_overrated
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 be_overrated }
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 be_overrated
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 be_overrated
152
+ expect(limiter).to be_overloaded
151
153
 
152
154
  Timecop.freeze(1)
153
- expect(limiter).not_to be_overrated
155
+ expect(limiter).not_to be_overloaded
154
156
  end
155
157
 
156
- it 'can be a Float' do
157
- 2.times { limiter.limit(cost: 1.5) }
158
- expect(limiter).to be_overrated
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 be_overrated
211
+ expect(limiter_one.limit).not_to be_overloaded
168
212
 
169
- expect(limiter_one).to be_overrated
170
- expect(limiter_two).to be_overrated
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 be_overrated
180
- expect(limiter_two.limit).not_to be_overrated
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 be_overrated
183
- expect(limiter_two.limit).not_to be_overrated
226
+ expect(limiter_one).to be_overloaded
227
+ expect(limiter_two.limit).not_to be_overloaded
184
228
 
185
- expect(limiter_one).to be_overrated
186
- expect(limiter_two).to be_overrated
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 '#overloaded?' do
192
- let(:limiter) { described_class.new(:key, 1, :second) }
235
+ describe '#utilization' do
236
+ let(:limiter) { described_class.new(:key, 10, :minute) }
193
237
 
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
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
- limiter.redis.decr(limiter.send(:cache_key, :key))
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
@@ -1,8 +1,4 @@
1
1
  describe Berater::TestMode do
2
- after do
3
- Berater.test_mode = nil
4
- end
5
-
6
2
  context 'after test_mode.rb has been loaded' do
7
3
  it 'monkey patches Berater' do
8
4
  expect(Berater).to respond_to(:test_mode)
@@ -13,12 +9,12 @@ describe Berater::TestMode do
13
9
  end
14
10
 
15
11
  it 'prepends Limiter subclasses' do
16
- expect(Berater::Unlimiter.ancestors).to include(described_class)
17
- expect(Berater::Inhibitor.ancestors).to include(described_class)
12
+ expect(Berater::Unlimiter.ancestors).to include(Berater::Limiter::TestMode)
13
+ expect(Berater::Inhibitor.ancestors).to include(Berater::Limiter::TestMode)
18
14
  end
19
15
 
20
16
  it 'preserves the original functionality via super' do
21
- expect { Berater::Limiter.new }.to raise_error(NotImplementedError)
17
+ expect { Berater::Limiter.new }.to raise_error(NoMethodError)
22
18
  end
23
19
  end
24
20
 
@@ -55,56 +51,50 @@ describe Berater::TestMode do
55
51
  end
56
52
  end
57
53
 
58
- shared_examples 'it always works, without redis' do
59
- before do
60
- Berater.redis = nil
61
- expect_any_instance_of(Berater::LuaScript).not_to receive(:eval)
62
- end
63
-
64
- it_behaves_like 'it is not overloaded'
54
+ describe '.reset' do
55
+ before { Berater.test_mode = :pass }
65
56
 
66
- it 'always works' do
67
- 10.times { subject.limit }
57
+ it 'resets test_mode' do
58
+ expect(Berater.test_mode).to be :pass
59
+ Berater.reset
60
+ expect(Berater.test_mode).to be nil
68
61
  end
69
62
  end
70
63
 
71
- shared_examples 'it never works, without redis' do
64
+ shared_examples 'it supports test_mode' do
72
65
  before do
66
+ # without hitting Redis
73
67
  Berater.redis = nil
74
68
  expect_any_instance_of(Berater::LuaScript).not_to receive(:eval)
75
69
  end
76
70
 
77
- it_behaves_like 'it is overloaded'
78
- end
79
-
80
- describe 'Unlimiter' do
81
- subject { Berater::Unlimiter.new }
71
+ context 'with test_mode = :pass' do
72
+ before { Berater.test_mode = :pass }
82
73
 
83
- context 'when test_mode = nil' do
84
- before { Berater.test_mode = nil }
74
+ it_behaves_like 'it is not overloaded'
85
75
 
86
- it { is_expected.to be_a Berater::Unlimiter }
87
- it_behaves_like 'it always works, without redis'
76
+ it 'always works' do
77
+ 10.times { subject.limit }
78
+ end
88
79
  end
89
80
 
90
- context 'when test_mode = :pass' do
91
- before { Berater.test_mode = :pass }
81
+ context 'with test_mode = :fail' do
82
+ before { Berater.test_mode = :fail }
92
83
 
93
- it { is_expected.to be_a Berater::Unlimiter }
94
- it_behaves_like 'it always works, without redis'
84
+ it_behaves_like 'it is overloaded'
95
85
  end
86
+ end
96
87
 
97
- context 'when test_mode = :fail' do
98
- before { Berater.test_mode = :fail }
88
+ describe 'Unlimiter' do
89
+ subject { Berater::Unlimiter.new }
99
90
 
100
- it { is_expected.to be_a Berater::Unlimiter }
101
- it_behaves_like 'it never works, without redis'
91
+ context 'when test_mode = nil' do
92
+ before { Berater.test_mode = nil }
102
93
 
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
94
+ it_behaves_like 'it is not overloaded'
107
95
  end
96
+
97
+ it_behaves_like 'it supports test_mode'
108
98
  end
109
99
 
110
100
  describe 'Inhibitor' do
@@ -113,47 +103,18 @@ describe Berater::TestMode do
113
103
  context 'when test_mode = nil' do
114
104
  before { Berater.test_mode = nil }
115
105
 
116
- it { is_expected.to be_a Berater::Inhibitor }
117
- it_behaves_like 'it never works, without redis'
106
+ it_behaves_like 'it is overloaded'
118
107
  end
119
108
 
120
- context 'when test_mode = :pass' do
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
109
+ it_behaves_like 'it supports test_mode'
138
110
  end
139
111
 
140
112
  describe 'RateLimiter' do
141
113
  subject { Berater::RateLimiter.new(:key, 1, :second) }
142
114
 
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
115
  context 'when test_mode = nil' do
154
116
  before { Berater.test_mode = nil }
155
117
 
156
- it_behaves_like 'a RateLimiter'
157
118
  it_behaves_like 'it is not overloaded'
158
119
 
159
120
  it 'works per usual' do
@@ -163,24 +124,7 @@ describe Berater::TestMode do
163
124
  end
164
125
  end
165
126
 
166
- context 'when test_mode = :pass' do
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
127
+ it_behaves_like 'it supports test_mode'
184
128
  end
185
129
 
186
130
  describe 'ConcurrencyLimiter' do
@@ -189,8 +133,6 @@ describe Berater::TestMode do
189
133
  context 'when test_mode = nil' do
190
134
  before { Berater.test_mode = nil }
191
135
 
192
- it { is_expected.to be_a Berater::ConcurrencyLimiter }
193
-
194
136
  it_behaves_like 'it is not overloaded'
195
137
 
196
138
  it 'works per usual' do
@@ -200,26 +142,7 @@ describe Berater::TestMode do
200
142
  end
201
143
  end
202
144
 
203
- context 'when test_mode = :pass' do
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
145
+ it_behaves_like 'it supports test_mode'
223
146
  end
224
147
 
225
148
  end