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.
data/lib/berater/rspec.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  require 'berater'
2
2
  require 'berater/rspec/matchers'
3
3
  require 'berater/test_mode'
4
- require 'rspec'
4
+ require 'rspec/core'
5
5
 
6
6
  RSpec.configure do |config|
7
- config.include(BeraterMatchers)
7
+ config.include(Berater::Matchers)
8
8
 
9
9
  config.after do
10
10
  Berater.expunge rescue nil
@@ -1,23 +1,20 @@
1
- module BeraterMatchers
2
- class Overloaded
3
- def initialize(type)
4
- @type = type
5
- end
6
-
7
- def supports_block_expectations?
8
- true
9
- end
1
+ module Berater
2
+ module Matchers
3
+ class Overloaded
4
+ def supports_block_expectations?
5
+ true
6
+ end
10
7
 
11
- def matches?(obj)
12
- begin
8
+ def matches?(obj)
13
9
  case obj
14
10
  when Proc
15
- # eg. expect { ... }.to be_overrated
11
+ # eg. expect { ... }.to be_overloaded
16
12
  res = obj.call
17
13
 
18
14
  if res.is_a? Berater::Limiter
19
15
  # eg. expect { Berater.new(...) }.to be_overloaded
20
- res.overloaded?
16
+ @limiter = res
17
+ @limiter.utilization >= 1
21
18
  else
22
19
  # eg. expect { Berater(...) }.to be_overloaded
23
20
  # eg. expect { limiter.limit }.to be_overloaded
@@ -25,38 +22,40 @@ module BeraterMatchers
25
22
  end
26
23
  when Berater::Limiter
27
24
  # eg. expect(Berater.new(...)).to be_overloaded
28
- obj.overloaded?
25
+ @limiter = obj
26
+ @limiter.utilization >= 1
29
27
  end
30
- rescue @type
28
+ rescue Berater::Overloaded
31
29
  true
32
30
  end
33
- end
34
31
 
35
- # def description
36
- # it { expect { Berater.new(:inhibitor) }.not_to be_overrated }
32
+ def description
33
+ if @limiter
34
+ "be overloaded"
35
+ else
36
+ "raise #{Berater::Overloaded}"
37
+ end
38
+ end
37
39
 
38
- def failure_message
39
- "expected #{@type} to be raised"
40
- end
40
+ def failure_message
41
+ if @limiter
42
+ "expected to be overloaded"
43
+ else
44
+ "expected #{Berater::Overloaded} to be raised"
45
+ end
46
+ end
41
47
 
42
- def failure_message_when_negated
43
- "did not expect #{@type} to be raised"
48
+ def failure_message_when_negated
49
+ if @limiter
50
+ "expected not to be overloaded"
51
+ else
52
+ "did not expect #{Berater::Overloaded} to be raised"
53
+ end
54
+ end
44
55
  end
45
- end
46
-
47
- def be_overloaded
48
- Overloaded.new(Berater::Overloaded)
49
- end
50
56
 
51
- def be_overrated
52
- Overloaded.new(Berater::RateLimiter::Overrated)
53
- end
54
-
55
- def be_incapacitated
56
- Overloaded.new(Berater::ConcurrencyLimiter::Incapacitated)
57
- end
58
-
59
- def be_inhibited
60
- Overloaded.new(Berater::Inhibitor::Inhibited)
57
+ def be_overloaded
58
+ Overloaded.new
59
+ end
61
60
  end
62
61
  end
@@ -11,42 +11,26 @@ module Berater
11
11
  end
12
12
 
13
13
  @test_mode = mode
14
-
15
- # overload class methods
16
- unless Berater::Limiter.singleton_class.ancestors.include?(TestMode)
17
- Berater::Limiter.singleton_class.prepend(TestMode)
18
- end
19
14
  end
20
15
 
21
16
  module TestMode
22
- def new(*args, **opts)
23
- return super unless Berater.test_mode
24
-
25
- # chose a stub class with desired behavior
26
- stub_klass = case Berater.test_mode
17
+ def acquire_lock(*)
18
+ case Berater.test_mode
27
19
  when :pass
28
- Berater::Unlimiter
20
+ Lock.new(Float::INFINITY, 0)
29
21
  when :fail
30
- Berater::Inhibitor
31
- end
32
-
33
- # don't stub self
34
- return super if self < stub_klass
35
-
36
- # swap out limit and overloaded? methods with stub
37
- super.tap do |instance|
38
- stub = stub_klass.allocate
39
- stub.send(:initialize, *args, **opts)
40
-
41
- instance.define_singleton_method(:limit) do |**opts, &block|
42
- stub.limit(**opts, &block)
43
- end
44
-
45
- instance.define_singleton_method(:overloaded?) do
46
- stub.overloaded?
47
- end
22
+ raise Overloaded
23
+ else
24
+ super
48
25
  end
49
26
  end
50
27
  end
51
28
 
52
29
  end
30
+
31
+ # stub each Limiter subclass
32
+ ObjectSpace.each_object(Class).each do |klass|
33
+ next unless klass < Berater::Limiter
34
+
35
+ klass.prepend(Berater::TestMode)
36
+ end
@@ -5,12 +5,14 @@ module Berater
5
5
  super(key, Float::INFINITY, **opts)
6
6
  end
7
7
 
8
- def limit(**opts, &block)
9
- yield_lock(Lock.new(self, Float::INFINITY, 0), &block)
8
+ protected
9
+
10
+ def capacity=(*)
11
+ @capacity = Float::INFINITY
10
12
  end
11
13
 
12
- def overloaded?
13
- false
14
+ def acquire_lock(*)
15
+ Lock.new(Float::INFINITY, 0)
14
16
  end
15
17
 
16
18
  end
data/lib/berater/utils.rb CHANGED
@@ -3,12 +3,12 @@ module Berater
3
3
  extend self
4
4
 
5
5
  refine Object do
6
- def to_usec
7
- Berater::Utils.to_usec(self)
6
+ def to_msec
7
+ Berater::Utils.to_msec(self)
8
8
  end
9
9
  end
10
10
 
11
- def to_usec(val)
11
+ def to_msec(val)
12
12
  res = val
13
13
 
14
14
  if val.is_a? String
@@ -39,7 +39,7 @@ module Berater
39
39
  raise ArgumentError, "infinite values not allowed"
40
40
  end
41
41
 
42
- (res * 10**6).to_i
42
+ (res * 10**3).to_i
43
43
  end
44
44
 
45
45
  end
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.5.0'
2
+ VERSION = '0.7.1'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -25,7 +25,7 @@ describe Berater do
25
25
  end
26
26
 
27
27
  describe '.new' do
28
- context 'unlimited mode' do
28
+ context 'Unlimiter mode' do
29
29
  let(:limiter) { Berater.new(:key, Float::INFINITY) }
30
30
 
31
31
  it 'instantiates an Unlimiter' do
@@ -44,7 +44,7 @@ describe Berater do
44
44
  end
45
45
  end
46
46
 
47
- context 'inhibited mode' do
47
+ context 'Inhibitor mode' do
48
48
  let(:limiter) { Berater.new(:key, 0) }
49
49
 
50
50
  it 'instantiates an Inhibitor' do
@@ -64,7 +64,7 @@ describe Berater do
64
64
  end
65
65
 
66
66
  context 'rate mode' do
67
- let(:limiter) { Berater.new(:key, 1, :second) }
67
+ let(:limiter) { Berater.new(:key, 1, interval: :second) }
68
68
 
69
69
  it 'instantiates a RateLimiter' do
70
70
  expect(limiter).to be_a Berater::RateLimiter
@@ -77,7 +77,7 @@ describe Berater do
77
77
 
78
78
  it 'accepts options' do
79
79
  redis = double('Redis')
80
- limiter = Berater.new(:key, 1, :second, redis: redis)
80
+ limiter = Berater.new(:key, 1, interval: :second, redis: redis)
81
81
  expect(limiter.redis).to be redis
82
82
  end
83
83
  end
@@ -103,20 +103,20 @@ describe Berater do
103
103
  end
104
104
 
105
105
  describe 'Berater() - convenience method' do
106
- RSpec.shared_examples 'test convenience' do |klass, *args|
106
+ RSpec.shared_examples 'test convenience' do |klass, capacity, **opts|
107
107
  it 'creates a limiter' do
108
- limiter = Berater(:key, *args)
108
+ limiter = Berater(:key, capacity, **opts)
109
109
  expect(limiter).to be_a klass
110
110
  end
111
111
 
112
112
  context 'with a block' do
113
113
  it 'creates a limiter and calls limit' do
114
- limiter = Berater(:key, *args)
114
+ limiter = Berater(:key, capacity, **opts)
115
115
  expect(klass).to receive(:new).and_return(limiter)
116
116
  expect(limiter).to receive(:limit).and_call_original
117
117
 
118
118
  begin
119
- res = Berater(:key, *args) { true }
119
+ res = Berater(:key, capacity, **opts) { true }
120
120
  expect(res).to be true
121
121
  rescue Berater::Overloaded
122
122
  expect(klass).to be Berater::Inhibitor
@@ -127,7 +127,7 @@ describe Berater do
127
127
 
128
128
  include_examples 'test convenience', Berater::Unlimiter, Float::INFINITY
129
129
  include_examples 'test convenience', Berater::Inhibitor, 0
130
- include_examples 'test convenience', Berater::RateLimiter, 1, :second
130
+ include_examples 'test convenience', Berater::RateLimiter, 1, interval: :second
131
131
  include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
132
132
  end
133
133
 
@@ -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) }
@@ -42,16 +42,16 @@ describe Berater::ConcurrencyLimiter do
42
42
  describe '#timeout' do
43
43
  # see spec/utils_spec.rb
44
44
 
45
- it 'saves the interval in original and microsecond format' do
45
+ it 'saves the interval in original and millisecond format' do
46
46
  limiter = described_class.new(:key, 1, timeout: 3)
47
47
  expect(limiter.timeout).to be 3
48
- expect(limiter.instance_variable_get(:@timeout_usec)).to be (3 * 10**6)
48
+ expect(limiter.instance_variable_get(:@timeout_msec)).to be (3 * 10**3)
49
49
  end
50
50
 
51
51
  it 'handles infinity' do
52
52
  limiter = described_class.new(:key, 1, timeout: Float::INFINITY)
53
53
  expect(limiter.timeout).to be Float::INFINITY
54
- expect(limiter.instance_variable_get(:@timeout_usec)).to be 0
54
+ expect(limiter.instance_variable_get(:@timeout_msec)).to be 0
55
55
  end
56
56
  end
57
57
 
@@ -77,77 +77,104 @@ describe Berater::ConcurrencyLimiter do
77
77
  expect(limiter.limit).to be_a Berater::Lock
78
78
  expect(limiter.limit).to be_a Berater::Lock
79
79
 
80
- expect(limiter).to be_incapacitated
80
+ expect(limiter).to be_overloaded
81
81
  end
82
82
 
83
83
  context 'with capacity 0' do
84
84
  let(:limiter) { described_class.new(:key, 0) }
85
85
 
86
86
  it 'always fails' do
87
- expect(limiter).to be_incapacitated
87
+ expect(limiter).to be_overloaded
88
+ end
89
+ end
90
+
91
+ context 'when capacity is a Float' do
92
+ let(:limiter) { described_class.new(:key, 1.5) }
93
+
94
+ it 'still works' do
95
+ lock = limiter.limit
96
+
97
+ # since fractional cost is not supported
98
+ expect(lock.capacity).to be 1
99
+ expect(limiter).to be_overloaded
88
100
  end
89
101
  end
90
102
 
91
103
  it 'limit resets over time' do
92
104
  2.times { limiter.limit }
93
- expect(limiter).to be_incapacitated
105
+ expect(limiter).to be_overloaded
94
106
 
95
107
  Timecop.freeze(30)
96
108
 
97
109
  2.times { limiter.limit }
98
- expect(limiter).to be_incapacitated
110
+ expect(limiter).to be_overloaded
99
111
  end
100
112
 
101
113
  it 'limit resets with millisecond precision' do
102
114
  2.times { limiter.limit }
103
- expect(limiter).to be_incapacitated
115
+ expect(limiter).to be_overloaded
104
116
 
105
117
  # travel forward to just before first lock times out
106
118
  Timecop.freeze(29.999)
107
- expect(limiter).to be_incapacitated
119
+ expect(limiter).to be_overloaded
108
120
 
109
121
  # traveling one more millisecond will decrement the count
110
122
  Timecop.freeze(0.001)
111
123
  2.times { limiter.limit }
112
- expect(limiter).to be_incapacitated
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
113
131
  end
114
132
 
115
133
  context 'with cost parameter' do
116
- it { expect { limiter.limit(cost: 4) }.to be_incapacitated }
134
+ it { expect { limiter.limit(cost: 4) }.to be_overloaded }
117
135
 
118
136
  it 'works within limit' do
119
137
  limiter.limit(cost: 2)
120
- expect(limiter).to be_incapacitated
138
+ expect(limiter).to be_overloaded
121
139
  end
122
140
 
123
141
  it 'releases full cost' do
124
142
  lock = limiter.limit(cost: 2)
125
- expect(limiter).to be_incapacitated
143
+ expect(limiter).to be_overloaded
126
144
 
127
145
  lock.release
128
- expect(limiter).not_to be_incapacitated
146
+ expect(limiter).not_to be_overloaded
129
147
 
130
148
  lock = limiter.limit(cost: 2)
131
- expect(limiter).to be_incapacitated
149
+ expect(limiter).to be_overloaded
132
150
  end
133
151
 
134
152
  it 'respects timeout' do
135
153
  limiter.limit(cost: 2)
136
- expect(limiter).to be_incapacitated
154
+ expect(limiter).to be_overloaded
137
155
 
138
156
  Timecop.freeze(30)
139
- expect(limiter).not_to be_incapacitated
157
+ expect(limiter).not_to be_overloaded
140
158
 
141
159
  limiter.limit(cost: 2)
142
- expect(limiter).to be_incapacitated
160
+ expect(limiter).to be_overloaded
143
161
  end
144
162
 
145
- it 'accepts a dynamic capacity' do
146
- limiter = described_class.new(:key, 1)
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
147
175
 
148
- expect { limiter.limit(capacity: 0) }.to be_incapacitated
149
- 5.times { limiter.limit(capacity: 10) }
150
- expect { limiter }.to be_incapacitated
176
+ it 'only allows positive values' do
177
+ expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
151
178
  end
152
179
  end
153
180
 
@@ -160,8 +187,8 @@ describe Berater::ConcurrencyLimiter do
160
187
  it 'works as expected' do
161
188
  expect(limiter_one.limit).to be_a Berater::Lock
162
189
 
163
- expect(limiter_one).to be_incapacitated
164
- expect(limiter_two).to be_incapacitated
190
+ expect(limiter_one).to be_overloaded
191
+ expect(limiter_two).to be_overloaded
165
192
  end
166
193
  end
167
194
 
@@ -175,22 +202,22 @@ describe Berater::ConcurrencyLimiter do
175
202
  one_lock = limiter_one.limit
176
203
  expect(one_lock).to be_a Berater::Lock
177
204
 
178
- expect(limiter_one).to be_incapacitated
179
- expect(limiter_two).not_to be_incapacitated
205
+ expect(limiter_one).to be_overloaded
206
+ expect(limiter_two).not_to be_overloaded
180
207
 
181
208
  two_lock = limiter_two.limit
182
209
  expect(two_lock).to be_a Berater::Lock
183
210
 
184
- expect(limiter_one).to be_incapacitated
185
- expect(limiter_two).to be_incapacitated
211
+ expect(limiter_one).to be_overloaded
212
+ expect(limiter_two).to be_overloaded
186
213
 
187
214
  one_lock.release
188
- expect(limiter_one).to be_incapacitated
189
- expect(limiter_two).not_to be_incapacitated
215
+ expect(limiter_one).to be_overloaded
216
+ expect(limiter_two).not_to be_overloaded
190
217
 
191
218
  two_lock.release
192
- expect(limiter_one).not_to be_incapacitated
193
- expect(limiter_two).not_to be_incapacitated
219
+ expect(limiter_one).not_to be_overloaded
220
+ expect(limiter_two).not_to be_overloaded
194
221
  end
195
222
  end
196
223
 
@@ -199,41 +226,41 @@ describe Berater::ConcurrencyLimiter do
199
226
  let(:limiter_two) { described_class.new(:two, 1) }
200
227
 
201
228
  it 'works as expected' do
202
- expect(limiter_one).not_to be_incapacitated
203
- expect(limiter_two).not_to be_incapacitated
229
+ expect(limiter_one).not_to be_overloaded
230
+ expect(limiter_two).not_to be_overloaded
204
231
 
205
232
  one_lock = limiter_one.limit
206
233
  expect(one_lock).to be_a Berater::Lock
207
234
 
208
- expect(limiter_one).to be_incapacitated
209
- expect(limiter_two).not_to be_incapacitated
235
+ expect(limiter_one).to be_overloaded
236
+ expect(limiter_two).not_to be_overloaded
210
237
 
211
238
  two_lock = limiter_two.limit
212
239
  expect(two_lock).to be_a Berater::Lock
213
240
 
214
- expect(limiter_one).to be_incapacitated
215
- expect(limiter_two).to be_incapacitated
241
+ expect(limiter_one).to be_overloaded
242
+ expect(limiter_two).to be_overloaded
216
243
  end
217
244
  end
218
245
  end
219
246
 
220
- describe '#overloaded?' do
221
- let(:limiter) { described_class.new(:key, 1, timeout: 30) }
247
+ describe '#utilization' do
248
+ let(:limiter) { described_class.new(:key, 10, timeout: 30) }
222
249
 
223
250
  it 'works' do
224
- expect(limiter.overloaded?).to be false
225
- lock = limiter.limit
226
- expect(limiter.overloaded?).to be true
227
- lock.release
228
- expect(limiter.overloaded?).to be false
229
- end
251
+ expect(limiter.utilization).to be 0.0
230
252
 
231
- it 'respects timeout' do
232
- expect(limiter.overloaded?).to be false
233
- lock = limiter.limit
234
- expect(limiter.overloaded?).to be true
235
- Timecop.freeze(30)
236
- expect(limiter.overloaded?).to be false
253
+ 2.times { limiter.limit }
254
+ expect(limiter.utilization).to be 0.2
255
+
256
+ Timecop.freeze(15)
257
+
258
+ 8.times { limiter.limit }
259
+ expect(limiter.utilization).to be 1.0
260
+
261
+ Timecop.freeze(15)
262
+
263
+ expect(limiter.utilization).to be 0.8
237
264
  end
238
265
  end
239
266