berater 0.6.1 → 0.9.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.
@@ -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