berater 0.5.0 → 0.7.1

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