berater 0.5.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,18 +3,6 @@ require 'berater/dsl'
3
3
  describe Berater do
4
4
  using Berater::DSL
5
5
 
6
- it 'instatiates an Unlimiter' do
7
- limiter = Berater.new(:key) { unlimited }
8
- expect(limiter).to be_a Berater::Unlimiter
9
- expect(limiter.key).to be :key
10
- end
11
-
12
- it 'instatiates an Inhibiter' do
13
- limiter = Berater.new(:key) { inhibited }
14
- expect(limiter).to be_a Berater::Inhibitor
15
- expect(limiter.key).to be :key
16
- end
17
-
18
6
  it 'instatiates a RateLimiter' do
19
7
  limiter = Berater.new(:key) { 1.per second }
20
8
  expect(limiter).to be_a Berater::RateLimiter
data/spec/dsl_spec.rb CHANGED
@@ -13,13 +13,13 @@ describe Berater::DSL do
13
13
  end
14
14
 
15
15
  it 'parses' do
16
- check([ 1, :second ]) { 1.per second }
17
- check([ 3, :minute ]) { 3.per minute }
18
- check([ 5, :hour ]) { 5.every hour }
16
+ check([ 1, interval: :second ]) { 1.per second }
17
+ check([ 3, interval: :minute ]) { 3.per minute }
18
+ check([ 5, interval: :hour ]) { 5.every hour }
19
19
  end
20
20
 
21
21
  it 'cleans up afterward' do
22
- check([ 1, :second ]) { 1.per second }
22
+ check([ 1, interval: :second ]) { 1.per second }
23
23
 
24
24
  expect(Integer).not_to respond_to(:per)
25
25
  expect(Integer).not_to respond_to(:every)
@@ -29,7 +29,7 @@ describe Berater::DSL do
29
29
  count = 1
30
30
  interval = :second
31
31
 
32
- check([ count, interval ]) { count.per interval }
32
+ check([ count, interval: interval ]) { count.per interval }
33
33
  end
34
34
  end
35
35
 
@@ -57,16 +57,4 @@ describe Berater::DSL do
57
57
  end
58
58
  end
59
59
 
60
- context 'unlimited mode' do
61
- it 'has keywords' do
62
- check(:unlimited) { unlimited }
63
- end
64
- end
65
-
66
- context 'inhibited mode' do
67
- it 'has keywords' do
68
- check(:inhibited) { inhibited }
69
- end
70
- end
71
-
72
60
  end
data/spec/limiter_spec.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  describe Berater::Limiter do
2
2
  it 'can not be initialized' do
3
- expect { described_class.new }.to raise_error(NotImplementedError)
3
+ expect { described_class.new }.to raise_error(NoMethodError)
4
4
  end
5
5
 
6
6
  describe 'abstract methods' do
@@ -8,11 +8,47 @@ describe Berater::Limiter do
8
8
 
9
9
  it do
10
10
  expect { limiter.limit }.to raise_error(NotImplementedError)
11
- expect { limiter.overloaded? }.to raise_error(NotImplementedError)
11
+ expect { limiter.utilization }.to raise_error(NotImplementedError)
12
12
  end
13
13
  end
14
14
 
15
- describe '==' do
15
+ describe '#limit' do
16
+ subject { Berater::Unlimiter.new }
17
+
18
+ context 'with a capacity parameter' do
19
+ it 'overrides the stored value' do
20
+ is_expected.to receive(:acquire_lock).with(3, anything)
21
+
22
+ subject.limit(capacity: 3)
23
+ end
24
+
25
+ it 'validates the type' do
26
+ expect {
27
+ subject.limit(capacity: 'abc')
28
+ }.to raise_error(ArgumentError)
29
+ end
30
+ end
31
+
32
+ context 'with a cost parameter' do
33
+ it 'overrides the stored value' do
34
+ is_expected.to receive(:acquire_lock).with(anything, 2)
35
+
36
+ subject.limit(cost: 2)
37
+ end
38
+
39
+ it 'validates' do
40
+ expect {
41
+ subject.limit(cost: 'abc')
42
+ }.to raise_error(ArgumentError)
43
+
44
+ expect {
45
+ subject.limit(cost: -1)
46
+ }.to raise_error(ArgumentError)
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '#==' do
16
52
  let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
17
53
 
18
54
  it 'equals itself' do
@@ -1,130 +1,122 @@
1
- describe 'be_overloaded' do
1
+ describe Berater::Matchers::Overloaded do
2
+
2
3
  context 'Berater::Unlimiter' do
3
- let(:limiter) { Berater.new(:key, :unlimited) }
4
+ let(:limiter) { Berater::Unlimiter.new }
4
5
 
5
6
  it { expect(limiter).not_to be_overloaded }
6
- it { expect(limiter).not_to be_inhibited }
7
- it { expect(limiter).not_to be_overrated }
8
- it { expect(limiter).not_to be_incapacitated }
9
-
10
7
  it { expect { limiter }.not_to be_overloaded }
11
- it { expect { limiter }.not_to be_inhibited }
12
- it { expect { limiter }.not_to be_overrated }
13
- it { expect { limiter }.not_to be_incapacitated }
14
-
15
8
  it { expect { limiter.limit }.not_to be_overloaded }
16
- it { expect { limiter.limit }.not_to be_inhibited }
17
- it { expect { limiter.limit }.not_to be_overrated }
18
- it { expect { limiter.limit }.not_to be_incapacitated }
19
-
20
- it 'catches false positives' do
21
- expect {
22
- expect { limiter }.to be_overloaded
23
- }.to fail
24
- end
25
9
  end
26
10
 
27
11
  context 'Berater::Inhibitor' do
28
- let(:limiter) { Berater.new(:key, :inhibited) }
12
+ let(:limiter) { Berater::Inhibitor.new }
29
13
 
30
14
  it { expect(limiter).to be_overloaded }
31
- it { expect(limiter).to be_inhibited }
32
-
33
15
  it { expect { limiter }.to be_overloaded }
34
- it { expect { limiter }.to be_inhibited }
35
-
36
16
  it { expect { limiter.limit }.to be_overloaded }
37
- it { expect { limiter.limit }.to be_inhibited }
38
-
39
- it 'catches false negatives' do
40
- expect {
41
- expect { limiter }.not_to be_overloaded
42
- }.to fail
43
- end
44
17
  end
45
18
 
46
19
  context 'Berater::RateLimiter' do
47
- let(:limiter) { Berater.new(:key, 1, :second) }
20
+ let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
48
21
 
49
22
  it { expect(limiter).not_to be_overloaded }
50
- it { expect(limiter).not_to be_inhibited }
51
- it { expect(limiter).not_to be_overrated }
52
- it { expect(limiter).not_to be_incapacitated }
53
-
54
23
  it { expect { limiter }.not_to be_overloaded }
55
- it { expect { limiter }.not_to be_inhibited }
56
- it { expect { limiter }.not_to be_overrated }
57
- it { expect { limiter }.not_to be_incapacitated }
58
-
59
24
  it { expect { limiter.limit }.not_to be_overloaded }
60
- it { expect { limiter.limit }.not_to be_inhibited }
61
- it { expect { limiter.limit }.not_to be_overrated }
62
- it { expect { limiter.limit }.not_to be_incapacitated }
63
25
 
64
26
  context 'once limit is used up' do
65
27
  before { limiter.limit }
66
28
 
67
- it 'should be_overrated' do
68
- expect(limiter).to be_overrated
29
+ it 'should be_overloaded' do
30
+ expect(limiter).to be_overloaded
69
31
  end
70
32
 
71
- it 'should be_overrated' do
72
- expect { limiter }.to be_overrated
33
+ it 'should be_overloaded' do
34
+ expect { limiter }.to be_overloaded
73
35
  end
74
36
 
75
- it 'should be_overrated' do
76
- expect { limiter.limit }.to be_overrated
37
+ it 'should be_overloaded' do
38
+ expect { limiter.limit }.to be_overloaded
77
39
  end
78
40
  end
79
41
  end
80
42
 
81
43
  context 'Berater::ConcurrencyLimiter' do
82
- let(:limiter) { Berater.new(:key, 1) }
44
+ let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
83
45
 
84
46
  it { expect(limiter).not_to be_overloaded }
85
- it { expect(limiter).not_to be_inhibited }
86
- it { expect(limiter).not_to be_overrated }
87
- it { expect(limiter).not_to be_incapacitated }
88
-
89
47
  it { expect { limiter }.not_to be_overloaded }
90
- it { expect { limiter }.not_to be_inhibited }
91
- it { expect { limiter }.not_to be_overrated }
92
- it { expect { limiter }.not_to be_incapacitated }
93
-
94
48
  it { expect { limiter.limit }.not_to be_overloaded }
95
- it { expect { limiter.limit }.not_to be_inhibited }
96
- it { expect { limiter.limit }.not_to be_overrated }
97
- it { expect { limiter.limit }.not_to be_incapacitated }
98
49
 
99
50
  context 'when lock is released' do
100
- it 'should be_incapacitated' do
51
+ it 'should be_overloaded' do
101
52
  3.times do
102
- expect(limiter).not_to be_incapacitated
53
+ expect(limiter).not_to be_overloaded
103
54
  end
104
55
  end
105
56
 
106
- it 'should be_incapacitated' do
57
+ it 'should be_overloaded' do
107
58
  3.times do
108
- expect { limiter }.not_to be_incapacitated
59
+ expect { limiter }.not_to be_overloaded
109
60
  end
110
61
  end
111
62
 
112
- it 'should be_incapacitated' do
63
+ it 'should be_overloaded' do
113
64
  3.times do
114
- expect { limiter.limit {} }.not_to be_incapacitated
65
+ expect { limiter.limit {} }.not_to be_overloaded
115
66
  end
116
67
  end
117
68
  end
118
69
 
119
70
  context 'when lock is *not* released' do
120
- it 'should be_incapacitated' do
121
- expect { limiter.limit }.not_to be_incapacitated
122
- 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
123
74
  end
124
75
 
125
- it 'should be_incapacitated' do
126
- expect { 3.times { limiter.limit } }.to be_incapacitated
76
+ it 'should be_overloaded' do
77
+ expect { 3.times { limiter.limit } }.to be_overloaded
127
78
  end
128
79
  end
129
80
  end
81
+
82
+ context 'when matchers fail' do
83
+ let(:unlimiter) { Berater::Unlimiter.new }
84
+ let(:inhibitor) { Berater::Inhibitor.new }
85
+
86
+ it 'catches false negatives' do
87
+ expect {
88
+ expect(unlimiter).to be_overloaded
89
+ }.to fail_including('expected to be overloaded')
90
+
91
+ expect {
92
+ expect { unlimiter }.to be_overloaded
93
+ }.to fail_including('expected to be overloaded')
94
+
95
+ expect {
96
+ expect { unlimiter.limit }.to be_overloaded
97
+ }.to fail_including("expected #{Berater::Overloaded} to be raised")
98
+
99
+ expect {
100
+ expect { 123 }.to be_overloaded
101
+ }.to fail_including("expected #{Berater::Overloaded} to be raised")
102
+ end
103
+
104
+ it 'catches false positives' do
105
+ expect {
106
+ expect(inhibitor).not_to be_overloaded
107
+ }.to fail_including('expected not to be overloaded')
108
+
109
+ expect {
110
+ expect { inhibitor }.not_to be_overloaded
111
+ }.to fail_including('expected not to be overloaded')
112
+
113
+ expect {
114
+ expect { inhibitor.limit }.not_to be_overloaded
115
+ }.to fail_including("did not expect #{Berater::Overloaded} to be raised")
116
+
117
+ expect {
118
+ expect { raise Berater::Overloaded }.not_to be_overloaded
119
+ }.to fail_including("did not expect #{Berater::Overloaded} to be raised")
120
+ end
121
+ end
130
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) }
@@ -23,6 +24,7 @@ describe Berater::RateLimiter do
23
24
 
24
25
  it { expect_capacity(0) }
25
26
  it { expect_capacity(1) }
27
+ it { expect_capacity(1.5) }
26
28
  it { expect_capacity(100) }
27
29
 
28
30
  context 'with erroneous values' do
@@ -32,7 +34,6 @@ describe Berater::RateLimiter do
32
34
  end.to raise_error ArgumentError
33
35
  end
34
36
 
35
- it { expect_bad_capacity(0.5) }
36
37
  it { expect_bad_capacity(-1) }
37
38
  it { expect_bad_capacity('1') }
38
39
  it { expect_bad_capacity(:one) }
@@ -44,9 +45,19 @@ describe Berater::RateLimiter do
44
45
 
45
46
  subject { described_class.new(:key, 1, :second) }
46
47
 
47
- it 'saves the interval in original and microsecond format' do
48
+ it 'saves the interval in original and millisecond format' do
48
49
  expect(subject.interval).to be :second
49
- expect(subject.instance_variable_get(:@interval_usec)).to be 10**6
50
+ expect(subject.instance_variable_get(:@interval_msec)).to be 10**3
51
+ end
52
+
53
+ it 'must be > 0' do
54
+ expect {
55
+ described_class.new(:key, 1, 0)
56
+ }.to raise_error(ArgumentError)
57
+
58
+ expect {
59
+ described_class.new(:key, 1, -1)
60
+ }.to raise_error(ArgumentError)
50
61
  end
51
62
  end
52
63
 
@@ -65,56 +76,129 @@ describe Berater::RateLimiter do
65
76
  it 'limits excessive calls' do
66
77
  3.times { limiter.limit }
67
78
 
68
- expect(limiter).to be_overrated
79
+ expect(limiter).to be_overloaded
69
80
  end
70
81
 
71
- it 'limit resets over time, with millisecond precision' do
82
+ it 'resets limit over time' do
72
83
  3.times { limiter.limit }
73
- expect(limiter).to be_overrated
74
-
75
- # travel forward to just before the count decrements
76
- Timecop.freeze(0.333)
77
- expect(limiter).to be_overrated
78
-
79
- # traveling one more millisecond will decrement the count
80
- Timecop.freeze(0.001)
81
- limiter.limit
82
- expect(limiter).to be_overrated
84
+ expect(limiter).to be_overloaded
83
85
 
84
- # traveling 1 second will reset the count
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
90
+ end
91
+
92
+ context 'with millisecond precision' do
93
+ it 'resets limit over time' do
94
+ 3.times { limiter.limit }
95
+ expect(limiter).to be_overloaded
96
+
97
+ # travel forward to just before the count decrements
98
+ Timecop.freeze(0.333)
99
+ expect(limiter).to be_overloaded
100
+
101
+ # traveling one more millisecond will decrement the count
102
+ Timecop.freeze(0.001)
103
+ limiter.limit
104
+ expect(limiter).to be_overloaded
105
+ end
106
+
107
+ it 'works when drip rate is < 1 per millisecond' do
108
+ limiter = described_class.new(:key, 2_000, :second)
109
+
110
+ limiter.capacity.times { limiter.limit }
111
+ expect(limiter).to be_overloaded
112
+
113
+ Timecop.freeze(0.001)
114
+ expect(limiter).not_to be_overloaded
115
+
116
+ 2.times { limiter.limit }
117
+ end
118
+ end
119
+
120
+ context 'when capacity is a Float' do
121
+ let(:limiter) { described_class.new(:key, 1.5, :second) }
122
+
123
+ it 'still works' do
124
+ limiter.limit
125
+ expect(limiter).not_to be_overloaded
126
+
127
+ expect { limiter.limit }.to be_overloaded
128
+
129
+ limiter.limit(cost: 0.5)
130
+ end
89
131
  end
90
132
 
91
133
  it 'accepts a dynamic capacity' do
92
134
  limiter = described_class.new(:key, 1, :second)
93
135
 
94
- expect { limiter.limit(capacity: 0) }.to be_overrated
136
+ expect { limiter.limit(capacity: 0) }.to be_overloaded
95
137
  5.times { limiter.limit(capacity: 10) }
96
- expect { limiter }.to be_overrated
138
+ expect { limiter }.to be_overloaded
97
139
  end
98
140
 
99
141
  context 'works with cost parameter' do
100
- it { expect { limiter.limit(cost: 4) }.to be_overrated }
142
+ it { expect { limiter.limit(cost: 4) }.to be_overloaded }
101
143
 
102
144
  it 'works within limit' do
103
145
  limiter.limit(cost: 3)
104
- expect { limiter.limit }.to be_overrated
146
+ expect { limiter.limit }.to be_overloaded
105
147
  end
106
148
 
107
149
  it 'resets over time' do
108
150
  limiter.limit(cost: 3)
109
- expect(limiter).to be_overrated
151
+ expect(limiter).to be_overloaded
110
152
 
111
153
  Timecop.freeze(1)
112
- expect(limiter).not_to be_overrated
154
+ expect(limiter).not_to be_overloaded
155
+ end
156
+
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
113
187
  end
114
188
 
115
- it 'can be a Float' do
116
- 2.times { limiter.limit(cost: 1.5) }
117
- expect(limiter).to be_overrated
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
118
202
  end
119
203
  end
120
204
 
@@ -123,10 +207,10 @@ describe Berater::RateLimiter do
123
207
  let(:limiter_two) { described_class.new(:key, 1, :second) }
124
208
 
125
209
  it 'works as expected' do
126
- expect(limiter_one.limit).not_to be_overrated
210
+ expect(limiter_one.limit).not_to be_overloaded
127
211
 
128
- expect(limiter_one).to be_overrated
129
- expect(limiter_two).to be_overrated
212
+ expect(limiter_one).to be_overloaded
213
+ expect(limiter_two).to be_overloaded
130
214
  end
131
215
  end
132
216
 
@@ -135,27 +219,33 @@ describe Berater::RateLimiter do
135
219
  let(:limiter_two) { described_class.new(:two, 2, :second) }
136
220
 
137
221
  it 'works as expected' do
138
- expect(limiter_one.limit).not_to be_overrated
139
- 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
140
224
 
141
- expect(limiter_one).to be_overrated
142
- expect(limiter_two.limit).not_to be_overrated
225
+ expect(limiter_one).to be_overloaded
226
+ expect(limiter_two.limit).not_to be_overloaded
143
227
 
144
- expect(limiter_one).to be_overrated
145
- expect(limiter_two).to be_overrated
228
+ expect(limiter_one).to be_overloaded
229
+ expect(limiter_two).to be_overloaded
146
230
  end
147
231
  end
148
232
  end
149
233
 
150
- describe '#overloaded?' do
151
- let(:limiter) { described_class.new(:key, 1, :second) }
234
+ describe '#utilization' do
235
+ let(:limiter) { described_class.new(:key, 10, :minute) }
152
236
 
153
- it 'works' do
154
- expect(limiter.overloaded?).to be false
155
- limiter.limit
156
- expect(limiter.overloaded?).to be true
157
- Timecop.freeze(1)
158
- 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
159
249
  end
160
250
  end
161
251