berater 0.6.0 → 0.8.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,118 +1,80 @@
1
1
  describe Berater::Matchers::Overloaded do
2
2
 
3
3
  context 'Berater::Unlimiter' do
4
- let(:limiter) { Berater.new(:key, :unlimited) }
4
+ let(:limiter) { Berater::Unlimiter.new }
5
5
 
6
6
  it { expect(limiter).not_to be_overloaded }
7
- it { expect(limiter).not_to be_inhibited }
8
- it { expect(limiter).not_to be_overrated }
9
- it { expect(limiter).not_to be_incapacitated }
10
-
11
7
  it { expect { limiter }.not_to be_overloaded }
12
- it { expect { limiter }.not_to be_inhibited }
13
- it { expect { limiter }.not_to be_overrated }
14
- it { expect { limiter }.not_to be_incapacitated }
15
-
16
8
  it { expect { limiter.limit }.not_to be_overloaded }
17
- it { expect { limiter.limit }.not_to be_inhibited }
18
- it { expect { limiter.limit }.not_to be_overrated }
19
- it { expect { limiter.limit }.not_to be_incapacitated }
20
9
  end
21
10
 
22
11
  context 'Berater::Inhibitor' do
23
- let(:limiter) { Berater.new(:key, :inhibited) }
12
+ let(:limiter) { Berater::Inhibitor.new }
24
13
 
25
14
  it { expect(limiter).to be_overloaded }
26
- it { expect(limiter).to be_inhibited }
27
-
28
15
  it { expect { limiter }.to be_overloaded }
29
- it { expect { limiter }.to be_inhibited }
30
-
31
16
  it { expect { limiter.limit }.to be_overloaded }
32
- it { expect { limiter.limit }.to be_inhibited }
33
17
  end
34
18
 
35
19
  context 'Berater::RateLimiter' do
36
- let(:limiter) { Berater.new(:key, 1, :second) }
20
+ let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
37
21
 
38
22
  it { expect(limiter).not_to be_overloaded }
39
- it { expect(limiter).not_to be_inhibited }
40
- it { expect(limiter).not_to be_overrated }
41
- it { expect(limiter).not_to be_incapacitated }
42
-
43
23
  it { expect { limiter }.not_to be_overloaded }
44
- it { expect { limiter }.not_to be_inhibited }
45
- it { expect { limiter }.not_to be_overrated }
46
- it { expect { limiter }.not_to be_incapacitated }
47
-
48
24
  it { expect { limiter.limit }.not_to be_overloaded }
49
- it { expect { limiter.limit }.not_to be_inhibited }
50
- it { expect { limiter.limit }.not_to be_overrated }
51
- it { expect { limiter.limit }.not_to be_incapacitated }
52
25
 
53
26
  context 'once limit is used up' do
54
27
  before { limiter.limit }
55
28
 
56
- it 'should be_overrated' do
57
- expect(limiter).to be_overrated
29
+ it 'should be_overloaded' do
30
+ expect(limiter).to be_overloaded
58
31
  end
59
32
 
60
- it 'should be_overrated' do
61
- expect { limiter }.to be_overrated
33
+ it 'should be_overloaded' do
34
+ expect { limiter }.to be_overloaded
62
35
  end
63
36
 
64
- it 'should be_overrated' do
65
- expect { limiter.limit }.to be_overrated
37
+ it 'should be_overloaded' do
38
+ expect { limiter.limit }.to be_overloaded
66
39
  end
67
40
  end
68
41
  end
69
42
 
70
43
  context 'Berater::ConcurrencyLimiter' do
71
- let(:limiter) { Berater.new(:key, 1) }
44
+ let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
72
45
 
73
46
  it { expect(limiter).not_to be_overloaded }
74
- it { expect(limiter).not_to be_inhibited }
75
- it { expect(limiter).not_to be_overrated }
76
- it { expect(limiter).not_to be_incapacitated }
77
-
78
47
  it { expect { limiter }.not_to be_overloaded }
79
- it { expect { limiter }.not_to be_inhibited }
80
- it { expect { limiter }.not_to be_overrated }
81
- it { expect { limiter }.not_to be_incapacitated }
82
-
83
48
  it { expect { limiter.limit }.not_to be_overloaded }
84
- it { expect { limiter.limit }.not_to be_inhibited }
85
- it { expect { limiter.limit }.not_to be_overrated }
86
- it { expect { limiter.limit }.not_to be_incapacitated }
87
49
 
88
50
  context 'when lock is released' do
89
- it 'should be_incapacitated' do
51
+ it 'should be_overloaded' do
90
52
  3.times do
91
- expect(limiter).not_to be_incapacitated
53
+ expect(limiter).not_to be_overloaded
92
54
  end
93
55
  end
94
56
 
95
- it 'should be_incapacitated' do
57
+ it 'should be_overloaded' do
96
58
  3.times do
97
- expect { limiter }.not_to be_incapacitated
59
+ expect { limiter }.not_to be_overloaded
98
60
  end
99
61
  end
100
62
 
101
- it 'should be_incapacitated' do
63
+ it 'should be_overloaded' do
102
64
  3.times do
103
- expect { limiter.limit {} }.not_to be_incapacitated
65
+ expect { limiter.limit {} }.not_to be_overloaded
104
66
  end
105
67
  end
106
68
  end
107
69
 
108
70
  context 'when lock is *not* released' do
109
- it 'should be_incapacitated' do
110
- expect { limiter.limit }.not_to be_incapacitated
111
- expect { limiter.limit }.to be_incapacitated
71
+ it 'should be_overloaded' do
72
+ expect { limiter.limit }.not_to be_overloaded
73
+ expect { limiter.limit }.to be_overloaded
112
74
  end
113
75
 
114
- it 'should be_incapacitated' do
115
- expect { 3.times { limiter.limit } }.to be_incapacitated
76
+ it 'should be_overloaded' do
77
+ expect { 3.times { limiter.limit } }.to be_overloaded
116
78
  end
117
79
  end
118
80
  end
@@ -156,31 +118,5 @@ describe Berater::Matchers::Overloaded do
156
118
  expect { raise Berater::Overloaded }.not_to be_overloaded
157
119
  }.to fail_including("did not expect #{Berater::Overloaded} to be raised")
158
120
  end
159
-
160
- it 'supports different verbs' do
161
- expect {
162
- expect { unlimiter }.to be_overrated
163
- }.to fail_including('expected to be overrated')
164
-
165
- expect {
166
- expect { unlimiter }.to be_incapacitated
167
- }.to fail_including('expected to be incapacitated')
168
- end
169
-
170
- it 'supports different exceptions' do
171
- expect {
172
- expect { 123 }.to be_overrated
173
- }.to fail_including(
174
- "expected #{Berater::RateLimiter::Overrated} to be raised"
175
- )
176
-
177
- expect {
178
- expect {
179
- raise Berater::ConcurrencyLimiter::Incapacitated
180
- }.not_to be_incapacitated
181
- }.to fail_including(
182
- "did not expect #{Berater::ConcurrencyLimiter::Incapacitated} to be raised"
183
- )
184
- end
185
121
  end
186
122
  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