berater 0.4.0 → 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,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.4.0'
2
+ VERSION = '0.7.0'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -7,12 +7,12 @@ describe Berater do
7
7
  it { is_expected.to respond_to :configure }
8
8
 
9
9
  describe '.configure' do
10
- it 'can be set via configure' do
10
+ it 'is used with a block' do
11
11
  Berater.configure do |c|
12
12
  c.redis = :redis
13
13
  end
14
14
 
15
- expect(Berater.redis).to eq :redis
15
+ expect(Berater.redis).to be :redis
16
16
  end
17
17
  end
18
18
 
@@ -25,8 +25,8 @@ describe Berater do
25
25
  end
26
26
 
27
27
  describe '.new' do
28
- context 'unlimited mode' do
29
- let(:limiter) { Berater.new(:key, :unlimited) }
28
+ context 'Unlimiter mode' do
29
+ let(:limiter) { Berater.new(:key, Float::INFINITY) }
30
30
 
31
31
  it 'instantiates an Unlimiter' do
32
32
  expect(limiter).to be_a Berater::Unlimiter
@@ -39,18 +39,13 @@ describe Berater do
39
39
 
40
40
  it 'accepts options' do
41
41
  redis = double('Redis')
42
- limiter = Berater.new(:key, :unlimited, redis: redis)
42
+ limiter = Berater.new(:key, Float::INFINITY, redis: redis)
43
43
  expect(limiter.redis).to be redis
44
44
  end
45
-
46
- it 'works with convinience' do
47
- expect(Berater).to receive(:new).and_return(limiter)
48
- expect {|b| Berater(:key, :unlimited, &b) }.to yield_control
49
- end
50
45
  end
51
46
 
52
- context 'inhibited mode' do
53
- let(:limiter) { Berater.new(:key, :inhibited) }
47
+ context 'Inhibitor mode' do
48
+ let(:limiter) { Berater.new(:key, 0) }
54
49
 
55
50
  it 'instantiates an Inhibitor' do
56
51
  expect(limiter).to be_a Berater::Inhibitor
@@ -63,18 +58,13 @@ describe Berater do
63
58
 
64
59
  it 'accepts options' do
65
60
  redis = double('Redis')
66
- limiter = Berater.new(:key, :inhibited, redis: redis)
61
+ limiter = Berater.new(:key, 0, redis: redis)
67
62
  expect(limiter.redis).to be redis
68
63
  end
69
-
70
- it 'works with convinience' do
71
- expect(Berater).to receive(:new).and_return(limiter)
72
- expect { Berater(:key, :inhibited) }.to be_inhibited
73
- end
74
64
  end
75
65
 
76
66
  context 'rate mode' do
77
- let(:limiter) { Berater.new(:key, :rate, 1, :second) }
67
+ let(:limiter) { Berater.new(:key, 1, interval: :second) }
78
68
 
79
69
  it 'instantiates a RateLimiter' do
80
70
  expect(limiter).to be_a Berater::RateLimiter
@@ -87,18 +77,13 @@ describe Berater do
87
77
 
88
78
  it 'accepts options' do
89
79
  redis = double('Redis')
90
- limiter = Berater.new(:key, :rate, 1, :second, redis: redis)
80
+ limiter = Berater.new(:key, 1, interval: :second, redis: redis)
91
81
  expect(limiter.redis).to be redis
92
82
  end
93
-
94
- it 'works with convinience' do
95
- expect(Berater).to receive(:new).and_return(limiter)
96
- expect {|b| Berater(:key, :rate, 1, :second, &b) }.to yield_control
97
- end
98
83
  end
99
84
 
100
85
  context 'concurrency mode' do
101
- let(:limiter) { Berater.new(:key, :concurrency, 1) }
86
+ let(:limiter) { Berater.new(:key, 1) }
102
87
 
103
88
  it 'instantiates a ConcurrencyLimiter' do
104
89
  expect(limiter).to be_a Berater::ConcurrencyLimiter
@@ -111,61 +96,39 @@ describe Berater do
111
96
 
112
97
  it 'accepts options' do
113
98
  redis = double('Redis')
114
- limiter = Berater.new(:key, :concurrency, 1, redis: redis)
99
+ limiter = Berater.new(:key, 1, redis: redis)
115
100
  expect(limiter.redis).to be redis
116
101
  end
117
-
118
- it 'works with convinience' do
119
- expect(Berater).to receive(:new).and_return(limiter)
120
- expect {|b| Berater(:key, :concurrency, 1, &b) }.to yield_control
121
- end
122
102
  end
103
+ end
123
104
 
124
- context 'with DSL' do
125
- it 'instatiates an Unlimiter' do
126
- limiter = Berater.new(:key) { unlimited }
127
- expect(limiter).to be_a Berater::Unlimiter
128
- expect(limiter.key).to be :key
129
- end
130
-
131
- it 'instatiates an Inhibiter' do
132
- limiter = Berater.new(:key) { inhibited }
133
- expect(limiter).to be_a Berater::Inhibitor
134
- expect(limiter.key).to be :key
135
- end
136
-
137
- it 'instatiates a RateLimiter' do
138
- limiter = Berater.new(:key) { 1.per second }
139
- expect(limiter).to be_a Berater::RateLimiter
140
- expect(limiter.key).to be :key
141
- expect(limiter.count).to be 1
142
- expect(limiter.interval).to be :second
143
- end
144
-
145
- it 'instatiates a ConcurrencyLimiter' do
146
- limiter = Berater.new(:key, timeout: 2) { 1.at_once }
147
- expect(limiter).to be_a Berater::ConcurrencyLimiter
148
- expect(limiter.key).to be :key
149
- expect(limiter.capacity).to be 1
150
- expect(limiter.timeout).to be 2
105
+ describe 'Berater() - convenience method' do
106
+ RSpec.shared_examples 'test convenience' do |klass, capacity, **opts|
107
+ it 'creates a limiter' do
108
+ limiter = Berater(:key, capacity, **opts)
109
+ expect(limiter).to be_a klass
151
110
  end
152
111
 
153
- it 'does not accept mode/args and dsl block' do
154
- expect {
155
- Berater.new(:key, :rate) { 1.per second }
156
- }.to raise_error(ArgumentError)
112
+ context 'with a block' do
113
+ it 'creates a limiter and calls limit' do
114
+ limiter = Berater(:key, capacity, **opts)
115
+ expect(klass).to receive(:new).and_return(limiter)
116
+ expect(limiter).to receive(:limit).and_call_original
157
117
 
158
- expect {
159
- Berater.new(:key, :concurrency, 2) { 3.at_once }
160
- }.to raise_error(ArgumentError)
161
- end
162
-
163
- it 'requires either mode or dsl block' do
164
- expect {
165
- Berater.new(:key)
166
- }.to raise_error(ArgumentError)
118
+ begin
119
+ res = Berater(:key, capacity, **opts) { true }
120
+ expect(res).to be true
121
+ rescue Berater::Overloaded
122
+ expect(klass).to be Berater::Inhibitor
123
+ end
124
+ end
167
125
  end
168
126
  end
127
+
128
+ include_examples 'test convenience', Berater::Unlimiter, Float::INFINITY
129
+ include_examples 'test convenience', Berater::Inhibitor, 0
130
+ include_examples 'test convenience', Berater::RateLimiter, 1, interval: :second
131
+ include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
169
132
  end
170
133
 
171
134
  end
@@ -18,11 +18,12 @@ describe Berater::ConcurrencyLimiter do
18
18
  describe '#capacity' do
19
19
  def expect_capacity(capacity)
20
20
  limiter = described_class.new(:key, capacity)
21
- expect(limiter.capacity).to eq capacity
21
+ expect(limiter.capacity).to eq capacity.to_i
22
22
  end
23
23
 
24
24
  it { expect_capacity(0) }
25
25
  it { expect_capacity(1) }
26
+ it { expect_capacity(1.5) }
26
27
  it { expect_capacity(10_000) }
27
28
 
28
29
  context 'with erroneous values' do
@@ -32,7 +33,6 @@ describe Berater::ConcurrencyLimiter do
32
33
  end.to raise_error ArgumentError
33
34
  end
34
35
 
35
- it { expect_bad_capacity(0.5) }
36
36
  it { expect_bad_capacity(-1) }
37
37
  it { expect_bad_capacity('1') }
38
38
  it { expect_bad_capacity(:one) }
@@ -40,31 +40,23 @@ describe Berater::ConcurrencyLimiter do
40
40
  end
41
41
 
42
42
  describe '#timeout' do
43
- def expect_timeout(timeout)
44
- limiter = described_class.new(:key, 1, timeout: timeout)
45
- expect(limiter.timeout).to eq timeout
46
- end
47
-
48
- it { expect_timeout(0) }
49
- it { expect_timeout(1) }
50
- it { expect_timeout(10_000) }
43
+ # see spec/utils_spec.rb
51
44
 
52
- context 'with erroneous values' do
53
- def expect_bad_timeout(timeout)
54
- expect do
55
- described_class.new(:key, 1, timeout: timeout)
56
- end.to raise_error ArgumentError
57
- end
45
+ it 'saves the interval in original and millisecond format' do
46
+ limiter = described_class.new(:key, 1, timeout: 3)
47
+ expect(limiter.timeout).to be 3
48
+ expect(limiter.instance_variable_get(:@timeout_msec)).to be (3 * 10**3)
49
+ end
58
50
 
59
- it { expect_bad_timeout(0.5) }
60
- it { expect_bad_timeout(-1) }
61
- it { expect_bad_timeout('1') }
62
- it { expect_bad_timeout(:one) }
51
+ it 'handles infinity' do
52
+ limiter = described_class.new(:key, 1, timeout: Float::INFINITY)
53
+ expect(limiter.timeout).to be Float::INFINITY
54
+ expect(limiter.instance_variable_get(:@timeout_msec)).to be 0
63
55
  end
64
56
  end
65
57
 
66
58
  describe '#limit' do
67
- let(:limiter) { described_class.new(:key, 2) }
59
+ let(:limiter) { described_class.new(:key, 2, timeout: 30) }
68
60
 
69
61
  it 'works' do
70
62
  expect {|b| limiter.limit(&b) }.to yield_control
@@ -85,80 +77,190 @@ describe Berater::ConcurrencyLimiter do
85
77
  expect(limiter.limit).to be_a Berater::Lock
86
78
  expect(limiter.limit).to be_a Berater::Lock
87
79
 
88
- expect(limiter).to be_incapacitated
80
+ expect(limiter).to be_overloaded
89
81
  end
90
82
 
91
83
  context 'with capacity 0' do
92
84
  let(:limiter) { described_class.new(:key, 0) }
93
85
 
94
86
  it 'always fails' do
95
- expect(limiter).to be_incapacitated
87
+ expect(limiter).to be_overloaded
96
88
  end
97
89
  end
98
- end
99
90
 
100
- context 'with same key, different limiters' do
101
- let(:limiter_one) { described_class.new(:key, 1) }
102
- let(:limiter_two) { described_class.new(:key, 1) }
91
+ context 'when capacity is a Float' do
92
+ let(:limiter) { described_class.new(:key, 1.5) }
103
93
 
104
- it { expect(limiter_one.key).to eq limiter_two.key }
94
+ it 'still works' do
95
+ lock = limiter.limit
105
96
 
106
- it 'works as expected' do
107
- expect(limiter_one.limit).to be_a Berater::Lock
97
+ # since fractional cost is not supported
98
+ expect(lock.capacity).to be 1
99
+ expect(limiter).to be_overloaded
100
+ end
101
+ end
108
102
 
109
- expect(limiter_one).to be_incapacitated
110
- expect(limiter_two).to be_incapacitated
103
+ it 'limit resets over time' do
104
+ 2.times { limiter.limit }
105
+ expect(limiter).to be_overloaded
106
+
107
+ Timecop.freeze(30)
108
+
109
+ 2.times { limiter.limit }
110
+ expect(limiter).to be_overloaded
111
111
  end
112
- end
113
112
 
114
- context 'with same key, different capacities' do
115
- let(:limiter_one) { described_class.new(:key, 1) }
116
- let(:limiter_two) { described_class.new(:key, 2) }
113
+ it 'limit resets with millisecond precision' do
114
+ 2.times { limiter.limit }
115
+ expect(limiter).to be_overloaded
116
+
117
+ # travel forward to just before first lock times out
118
+ Timecop.freeze(29.999)
119
+ expect(limiter).to be_overloaded
120
+
121
+ # traveling one more millisecond will decrement the count
122
+ Timecop.freeze(0.001)
123
+ 2.times { limiter.limit }
124
+ expect(limiter).to be_overloaded
125
+ end
126
+
127
+ it 'accepts a dynamic capacity' do
128
+ expect { limiter.limit(capacity: 0) }.to be_overloaded
129
+ 5.times { limiter.limit(capacity: 10) }
130
+ expect { limiter }.to be_overloaded
131
+ end
132
+
133
+ context 'with cost parameter' do
134
+ it { expect { limiter.limit(cost: 4) }.to be_overloaded }
135
+
136
+ it 'works within limit' do
137
+ limiter.limit(cost: 2)
138
+ expect(limiter).to be_overloaded
139
+ end
140
+
141
+ it 'releases full cost' do
142
+ lock = limiter.limit(cost: 2)
143
+ expect(limiter).to be_overloaded
144
+
145
+ lock.release
146
+ expect(limiter).not_to be_overloaded
147
+
148
+ lock = limiter.limit(cost: 2)
149
+ expect(limiter).to be_overloaded
150
+ end
151
+
152
+ it 'respects timeout' do
153
+ limiter.limit(cost: 2)
154
+ expect(limiter).to be_overloaded
117
155
 
118
- it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
156
+ Timecop.freeze(30)
157
+ expect(limiter).not_to be_overloaded
119
158
 
120
- it 'works as expected' do
121
- one_lock = limiter_one.limit
122
- expect(one_lock).to be_a Berater::Lock
159
+ limiter.limit(cost: 2)
160
+ expect(limiter).to be_overloaded
161
+ end
123
162
 
124
- expect(limiter_one).to be_incapacitated
125
- expect(limiter_two).not_to be_incapacitated
163
+ context 'with fractional costs' do
164
+ it 'rounds up' do
165
+ limiter.limit(cost: 1.5)
166
+ expect(limiter).to be_overloaded
167
+ end
168
+
169
+ it 'accumulates correctly' do
170
+ limiter.limit(cost: 0.5) # => 1
171
+ limiter.limit(cost: 0.7) # => 2
172
+ expect(limiter).to be_overloaded
173
+ end
174
+ end
126
175
 
127
- two_lock = limiter_two.limit
128
- expect(two_lock).to be_a Berater::Lock
176
+ it 'only allows positive values' do
177
+ expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
178
+ end
179
+ end
180
+
181
+ context 'with same key, different limiters' do
182
+ let(:limiter_one) { described_class.new(:key, 1) }
183
+ let(:limiter_two) { described_class.new(:key, 1) }
129
184
 
130
- expect(limiter_one).to be_incapacitated
131
- expect(limiter_two).to be_incapacitated
185
+ it { expect(limiter_one.key).to eq limiter_two.key }
132
186
 
133
- one_lock.release
134
- expect(limiter_one).to be_incapacitated
135
- expect(limiter_two).not_to be_incapacitated
187
+ it 'works as expected' do
188
+ expect(limiter_one.limit).to be_a Berater::Lock
136
189
 
137
- two_lock.release
138
- expect(limiter_one).not_to be_incapacitated
139
- expect(limiter_two).not_to be_incapacitated
190
+ expect(limiter_one).to be_overloaded
191
+ expect(limiter_two).to be_overloaded
192
+ end
193
+ end
194
+
195
+ context 'with same key, different capacities' do
196
+ let(:limiter_one) { described_class.new(:key, 1) }
197
+ let(:limiter_two) { described_class.new(:key, 2) }
198
+
199
+ it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
200
+
201
+ it 'works as expected' do
202
+ one_lock = limiter_one.limit
203
+ expect(one_lock).to be_a Berater::Lock
204
+
205
+ expect(limiter_one).to be_overloaded
206
+ expect(limiter_two).not_to be_overloaded
207
+
208
+ two_lock = limiter_two.limit
209
+ expect(two_lock).to be_a Berater::Lock
210
+
211
+ expect(limiter_one).to be_overloaded
212
+ expect(limiter_two).to be_overloaded
213
+
214
+ one_lock.release
215
+ expect(limiter_one).to be_overloaded
216
+ expect(limiter_two).not_to be_overloaded
217
+
218
+ two_lock.release
219
+ expect(limiter_one).not_to be_overloaded
220
+ expect(limiter_two).not_to be_overloaded
221
+ end
222
+ end
223
+
224
+ context 'with different keys, different limiters' do
225
+ let(:limiter_one) { described_class.new(:one, 1) }
226
+ let(:limiter_two) { described_class.new(:two, 1) }
227
+
228
+ it 'works as expected' do
229
+ expect(limiter_one).not_to be_overloaded
230
+ expect(limiter_two).not_to be_overloaded
231
+
232
+ one_lock = limiter_one.limit
233
+ expect(one_lock).to be_a Berater::Lock
234
+
235
+ expect(limiter_one).to be_overloaded
236
+ expect(limiter_two).not_to be_overloaded
237
+
238
+ two_lock = limiter_two.limit
239
+ expect(two_lock).to be_a Berater::Lock
240
+
241
+ expect(limiter_one).to be_overloaded
242
+ expect(limiter_two).to be_overloaded
243
+ end
140
244
  end
141
245
  end
142
246
 
143
- context 'with different keys, different limiters' do
144
- let(:limiter_one) { described_class.new(:one, 1) }
145
- let(:limiter_two) { described_class.new(:two, 1) }
247
+ describe '#utilization' do
248
+ let(:limiter) { described_class.new(:key, 10, timeout: 30) }
249
+
250
+ it 'works' do
251
+ expect(limiter.utilization).to be 0.0
146
252
 
147
- it 'works as expected' do
148
- expect(limiter_one).not_to be_incapacitated
149
- expect(limiter_two).not_to be_incapacitated
253
+ 2.times { limiter.limit }
254
+ expect(limiter.utilization).to be 0.2
150
255
 
151
- one_lock = limiter_one.limit
152
- expect(one_lock).to be_a Berater::Lock
256
+ Timecop.freeze(15)
153
257
 
154
- expect(limiter_one).to be_incapacitated
155
- expect(limiter_two).not_to be_incapacitated
258
+ 8.times { limiter.limit }
259
+ expect(limiter.utilization).to be 1.0
156
260
 
157
- two_lock = limiter_two.limit
158
- expect(two_lock).to be_a Berater::Lock
261
+ Timecop.freeze(15)
159
262
 
160
- expect(limiter_one).to be_incapacitated
161
- expect(limiter_two).to be_incapacitated
263
+ expect(limiter.utilization).to be 0.8
162
264
  end
163
265
  end
164
266