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.
@@ -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