berater 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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) }
@@ -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 be_overrated
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 be_overrated
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 be_overrated
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 be_overrated
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 be_overrated
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 be_overrated
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 be_overrated
111
+ expect(limiter).to be_overloaded
111
112
 
112
113
  Timecop.freeze(0.001)
113
- expect(limiter).not_to be_overrated
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 be_overrated
125
+ expect(limiter).not_to be_overloaded
125
126
 
126
- expect { limiter.limit }.to be_overrated
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 be_overrated
136
+ expect { limiter.limit(capacity: 0) }.to be_overloaded
136
137
  5.times { limiter.limit(capacity: 10) }
137
- expect { limiter }.to be_overrated
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 be_overrated }
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 be_overrated
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 be_overrated
151
+ expect(limiter).to be_overloaded
151
152
 
152
153
  Timecop.freeze(1)
153
- expect(limiter).not_to be_overrated
154
+ expect(limiter).not_to be_overloaded
154
155
  end
155
156
 
156
- it 'can be a Float' do
157
- 2.times { limiter.limit(cost: 1.5) }
158
- expect(limiter).to be_overrated
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 be_overrated
210
+ expect(limiter_one.limit).not_to be_overloaded
168
211
 
169
- expect(limiter_one).to be_overrated
170
- expect(limiter_two).to be_overrated
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 be_overrated
180
- expect(limiter_two.limit).not_to be_overrated
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 be_overrated
183
- expect(limiter_two.limit).not_to be_overrated
225
+ expect(limiter_one).to be_overloaded
226
+ expect(limiter_two.limit).not_to be_overloaded
184
227
 
185
- expect(limiter_one).to be_overrated
186
- expect(limiter_two).to be_overrated
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 '#overloaded?' do
192
- let(:limiter) { described_class.new(:key, 1, :second) }
234
+ describe '#utilization' do
235
+ let(:limiter) { described_class.new(:key, 10, :minute) }
193
236
 
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
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
- limiter.redis.decr(limiter.send(:cache_key, :key))
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
 
@@ -55,26 +55,28 @@ describe Berater::TestMode do
55
55
  end
56
56
  end
57
57
 
58
- shared_examples 'it always works, without redis' do
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
- it_behaves_like 'it is not overloaded'
65
+ context 'with test_mode = :pass' do
66
+ before { Berater.test_mode = :pass }
65
67
 
66
- it 'always works' do
67
- 10.times { subject.limit }
68
- end
69
- end
68
+ it_behaves_like 'it is not overloaded'
70
69
 
71
- shared_examples 'it never works, without redis' do
72
- before do
73
- Berater.redis = nil
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
- it_behaves_like 'it is overloaded'
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 { is_expected.to be_a Berater::Unlimiter }
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
- context 'when test_mode = :fail' do
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 { is_expected.to be_a Berater::Inhibitor }
117
- it_behaves_like 'it never works, without redis'
100
+ it_behaves_like 'it is overloaded'
118
101
  end
119
102
 
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
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
- 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
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
- 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
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.6.2
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-10 00:00:00.000000000 Z
11
+ date: 2021-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis