berater 0.6.0 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87d3bdb61a68a8b69dd7cb285dafb174a403acf19ff7586f90ed8b7cb50b7f76
4
- data.tar.gz: fff71dd4d8ae28baf7c504efdc27df71f03219b32c1a959dddfd3d035845d058
3
+ metadata.gz: 5c5cc50ba02c18bf9ef9f6b9c1dd6ccb8f8af0297df95757ac13406dc4539c40
4
+ data.tar.gz: f80341dccff30ca3f344f3bdf21de44b1ef1715bfaf4140933a91252ddb7231e
5
5
  SHA512:
6
- metadata.gz: c9fb5c0c7ec100d2f50a3b6c6cfa449af052c1b78cff174b545e179c5fcc1324c2a5faafc90e7f7ec64ce200e47571d51b02eb56398d716c487abc7731c0cece
7
- data.tar.gz: 148c566413d6593f229143e2871116b45947eae5b101d8bb2f59ce3bdfe89ed4ee1b7aaedfcaedbc80eef8a25a14fab83ad5e540172c41f31f27d7f296161235
6
+ metadata.gz: ada3fffc841e71498f652dc990eed482bccc942b86280fc5ee736d49fdf99d8ab2f921b5b52d96f3588c268c34343669c00e0be8822c462a3e6f9dcf954a4005
7
+ data.tar.gz: 0363abef69cfdad8140d89373b5088cfa8a6259f16716011c82ceb1e2e9e948cdb4d44ddd36aaf1d05b121dc7d275d0ee34571a0a3ee9b964f4286375ec356cf
@@ -63,15 +63,10 @@ module Berater
63
63
  LUA
64
64
  )
65
65
 
66
- def limit(capacity: nil, cost: 1, &block)
67
- capacity ||= @capacity
68
-
69
- # since fractional cost is not supported, capacity behaves like int
66
+ protected def acquire_lock(capacity, cost)
67
+ # fractional cost is not supported, but make it work
70
68
  capacity = capacity.to_i
71
-
72
- unless cost.is_a?(Integer) && cost >= 0
73
- raise ArgumentError, "invalid cost: #{cost}"
74
- end
69
+ cost = cost.ceil
75
70
 
76
71
  # timestamp in milliseconds
77
72
  ts = (Time.now.to_f * 10**3).to_i
@@ -87,16 +82,10 @@ module Berater
87
82
  release_fn = if cost > 0
88
83
  proc { release(lock_ids) }
89
84
  end
90
- lock = Lock.new(capacity, count, release_fn)
91
85
 
92
- yield_lock(lock, &block)
86
+ Lock.new(capacity, count, release_fn)
93
87
  end
94
88
 
95
- def overloaded?
96
- limit(cost: 0) { false }
97
- rescue Overloaded
98
- true
99
- end
100
89
  alias incapacitated? overloaded?
101
90
 
102
91
  private def release(lock_ids)
@@ -7,14 +7,11 @@ module Berater
7
7
  super(key, 0, **opts)
8
8
  end
9
9
 
10
- def limit(**opts)
11
- raise Inhibited
12
- end
10
+ alias inhibited? overloaded?
13
11
 
14
- def overloaded?
15
- true
12
+ protected def acquire_lock(*)
13
+ raise Inhibited
16
14
  end
17
- alias inhibited? overloaded?
18
15
 
19
16
  end
20
17
  end
@@ -7,12 +7,34 @@ module Berater
7
7
  options[:redis] || Berater.redis
8
8
  end
9
9
 
10
- def limit
11
- raise NotImplementedError
10
+ def limit(capacity: nil, cost: 1, &block)
11
+ capacity ||= @capacity
12
+
13
+ unless capacity.is_a?(Numeric)
14
+ raise ArgumentError, "invalid capacity: #{capacity}"
15
+ end
16
+
17
+ unless cost.is_a?(Numeric) && cost >= 0
18
+ raise ArgumentError, "invalid cost: #{cost}"
19
+ end
20
+
21
+ lock = acquire_lock(capacity, cost)
22
+
23
+ if block_given?
24
+ begin
25
+ yield lock
26
+ ensure
27
+ lock.release
28
+ end
29
+ else
30
+ lock
31
+ end
12
32
  end
13
33
 
14
34
  def overloaded?
15
- raise NotImplementedError
35
+ limit(cost: 0) { false }
36
+ rescue Overloaded
37
+ true
16
38
  end
17
39
 
18
40
  def to_s
@@ -60,20 +82,12 @@ module Berater
60
82
  @capacity = capacity
61
83
  end
62
84
 
63
- def cache_key(key)
64
- "#{self.class}:#{key}"
85
+ def acquire_lock(capacity, cost)
86
+ raise NotImplementedError
65
87
  end
66
88
 
67
- def yield_lock(lock, &block)
68
- if block_given?
69
- begin
70
- yield lock
71
- ensure
72
- lock.release
73
- end
74
- else
75
- lock
76
- end
89
+ def cache_key(key)
90
+ "#{self.class}:#{key}"
77
91
  end
78
92
 
79
93
  end
@@ -63,9 +63,7 @@ module Berater
63
63
  LUA
64
64
  )
65
65
 
66
- def limit(capacity: nil, cost: 1, &block)
67
- capacity ||= @capacity
68
-
66
+ protected def acquire_lock(capacity, cost)
69
67
  # timestamp in milliseconds
70
68
  ts = (Time.now.to_f * 10**3).to_i
71
69
 
@@ -77,15 +75,9 @@ module Berater
77
75
 
78
76
  raise Overrated unless allowed
79
77
 
80
- lock = Lock.new(capacity, count)
81
- yield_lock(lock, &block)
78
+ Lock.new(capacity, count)
82
79
  end
83
80
 
84
- def overloaded?
85
- limit(cost: 0) { false }
86
- rescue Overrated
87
- true
88
- end
89
81
  alias overrated? overloaded?
90
82
 
91
83
  def to_s
@@ -10,29 +10,27 @@ module Berater
10
10
  end
11
11
 
12
12
  def matches?(obj)
13
- begin
14
- case obj
15
- when Proc
16
- # eg. expect { ... }.to be_overrated
17
- res = obj.call
13
+ case obj
14
+ when Proc
15
+ # eg. expect { ... }.to be_overrated
16
+ res = obj.call
18
17
 
19
- if res.is_a? Berater::Limiter
20
- # eg. expect { Berater.new(...) }.to be_overloaded
21
- @limiter = res
22
- res.overloaded?
23
- else
24
- # eg. expect { Berater(...) }.to be_overloaded
25
- # eg. expect { limiter.limit }.to be_overloaded
26
- false
27
- end
28
- when Berater::Limiter
29
- # eg. expect(Berater.new(...)).to be_overloaded
30
- @limiter = obj
31
- obj.overloaded?
18
+ if res.is_a? Berater::Limiter
19
+ # eg. expect { Berater.new(...) }.to be_overloaded
20
+ @limiter = res
21
+ res.overloaded?
22
+ else
23
+ # eg. expect { Berater(...) }.to be_overloaded
24
+ # eg. expect { limiter.limit }.to be_overloaded
25
+ false
32
26
  end
33
- rescue @type
34
- true
27
+ when Berater::Limiter
28
+ # eg. expect(Berater.new(...)).to be_overloaded
29
+ @limiter = obj
30
+ obj.overloaded?
35
31
  end
32
+ rescue @type
33
+ true
36
34
  end
37
35
 
38
36
  def description
@@ -11,42 +11,33 @@ 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
+ # find class specific Overloaded error
23
+ e = self.class.constants.map do |name|
24
+ self.class.const_get(name)
25
+ end.find do |const|
26
+ const < Berater::Overloaded
27
+ end || Berater::Overloaded
28
+
29
+ raise e
30
+ else
31
+ super
48
32
  end
49
33
  end
50
34
  end
51
35
 
52
36
  end
37
+
38
+ # stub each Limiter subclass
39
+ ObjectSpace.each_object(Class).each do |klass|
40
+ next unless klass < Berater::Limiter
41
+
42
+ klass.prepend(Berater::TestMode)
43
+ end
@@ -5,16 +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(Float::INFINITY, 0), &block)
10
- end
8
+ protected
11
9
 
12
- def overloaded?
13
- false
10
+ def capacity=(*)
11
+ @capacity = Float::INFINITY
14
12
  end
15
13
 
16
- protected def capacity=(*)
17
- @capacity = Float::INFINITY
14
+ def acquire_lock(*)
15
+ Lock.new(Float::INFINITY, 0)
18
16
  end
19
17
 
20
18
  end
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.6.0'
2
+ VERSION = '0.6.1'
3
3
  end
@@ -124,6 +124,12 @@ describe Berater::ConcurrencyLimiter do
124
124
  expect(limiter).to be_incapacitated
125
125
  end
126
126
 
127
+ it 'accepts a dynamic capacity' do
128
+ expect { limiter.limit(capacity: 0) }.to be_incapacitated
129
+ 5.times { limiter.limit(capacity: 10) }
130
+ expect { limiter }.to be_incapacitated
131
+ end
132
+
127
133
  context 'with cost parameter' do
128
134
  it { expect { limiter.limit(cost: 4) }.to be_incapacitated }
129
135
 
@@ -154,17 +160,21 @@ describe Berater::ConcurrencyLimiter do
154
160
  expect(limiter).to be_incapacitated
155
161
  end
156
162
 
157
- it 'accepts a dynamic capacity' do
158
- limiter = described_class.new(:key, 1)
159
-
160
- expect { limiter.limit(capacity: 0) }.to be_incapacitated
161
- 5.times { limiter.limit(capacity: 10) }
162
- expect { limiter }.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_incapacitated
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_incapacitated
173
+ end
163
174
  end
164
175
 
165
- it 'only allows positive, Integer values' do
176
+ it 'only allows positive values' do
166
177
  expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
167
- expect { limiter.limit(cost: 1.5) }.to raise_error(ArgumentError)
168
178
  end
169
179
  end
170
180
 
@@ -1,31 +1,20 @@
1
- describe Berater::TestMode, order: :defined do
2
- let(:reset_test_mode) { true }
3
-
1
+ describe Berater::TestMode do
4
2
  after do
5
- Berater.test_mode = nil if reset_test_mode
3
+ Berater.test_mode = nil
6
4
  end
7
5
 
8
- context 'after test_mode.rb was required, but not used' do
9
- let(:reset_test_mode) { false }
10
-
11
- it 'has already been loaded by "berater/rspec", unfortunately' do
12
- expect {
13
- expect { Berater.test_mode }.to raise_error(NoMethodError)
14
- }.to fail
6
+ context 'after test_mode.rb has been loaded' do
7
+ it 'monkey patches Berater' do
8
+ expect(Berater).to respond_to(:test_mode)
15
9
  end
16
10
 
17
11
  it 'defaults to off' do
18
12
  expect(Berater.test_mode).to be nil
19
13
  end
20
14
 
21
- it 'did not prepend .new yet' do
22
- expect(Berater::Limiter.singleton_class.ancestors).not_to include(described_class)
23
- end
24
-
25
- it 'prepends when first turned on' do
26
- Berater.test_mode = :pass
27
-
28
- expect(Berater::Limiter.singleton_class.ancestors).to include(described_class)
15
+ it 'prepends Limiter subclasses' do
16
+ expect(Berater::Unlimiter.ancestors).to include(described_class)
17
+ expect(Berater::Inhibitor.ancestors).to include(described_class)
29
18
  end
30
19
 
31
20
  it 'preserves the original functionality via super' do
@@ -50,6 +39,20 @@ describe Berater::TestMode, order: :defined do
50
39
  it 'validates input' do
51
40
  expect { Berater.test_mode = :foo }.to raise_error(ArgumentError)
52
41
  end
42
+
43
+ it 'works no matter when limiter was created' do
44
+ limiter = Berater::Unlimiter.new
45
+ expect(limiter).not_to be_overloaded
46
+
47
+ Berater.test_mode = :fail
48
+ expect(limiter).to be_overloaded
49
+ end
50
+
51
+ it 'supports a generic expectation' do
52
+ Berater.test_mode = :pass
53
+ expect_any_instance_of(Berater::Limiter).to receive(:limit)
54
+ Berater::Unlimiter.new.limit
55
+ end
53
56
  end
54
57
 
55
58
  shared_examples 'it always works, without redis' do
@@ -72,10 +75,6 @@ describe Berater::TestMode, order: :defined do
72
75
  end
73
76
 
74
77
  it_behaves_like 'it is overloaded'
75
-
76
- it 'never works' do
77
- expect { subject }.to be_overloaded
78
- end
79
78
  end
80
79
 
81
80
  describe 'Unlimiter' do
@@ -100,6 +99,11 @@ describe Berater::TestMode, order: :defined do
100
99
 
101
100
  it { is_expected.to be_a Berater::Unlimiter }
102
101
  it_behaves_like 'it never works, without redis'
102
+
103
+ it 'supports class specific logic' do
104
+ expect(subject.overloaded?).to be true
105
+ expect { subject.limit }.to raise_error(Berater::Overloaded)
106
+ end
103
107
  end
104
108
  end
105
109
 
@@ -125,6 +129,11 @@ describe Berater::TestMode, order: :defined do
125
129
 
126
130
  it { is_expected.to be_a Berater::Inhibitor }
127
131
  it_behaves_like 'it never works, without redis'
132
+
133
+ it 'supports class specific logic' do
134
+ expect(subject.inhibited?).to be true
135
+ expect { subject.limit }.to raise_error(Berater::Inhibitor::Inhibited)
136
+ end
128
137
  end
129
138
  end
130
139
 
@@ -166,6 +175,11 @@ describe Berater::TestMode, order: :defined do
166
175
 
167
176
  it_behaves_like 'a RateLimiter'
168
177
  it_behaves_like 'it never works, without redis'
178
+
179
+ it 'supports class specific logic' do
180
+ expect(subject.overrated?).to be true
181
+ expect { subject.limit }.to raise_error(Berater::RateLimiter::Overrated)
182
+ end
169
183
  end
170
184
  end
171
185
 
@@ -200,6 +214,11 @@ describe Berater::TestMode, order: :defined do
200
214
  it { is_expected.to be_a Berater::ConcurrencyLimiter }
201
215
 
202
216
  it_behaves_like 'it never works, without redis'
217
+
218
+ it 'supports class specific logic' do
219
+ expect(subject.incapacitated?).to be true
220
+ expect { subject.limit }.to raise_error(Berater::ConcurrencyLimiter::Incapacitated)
221
+ end
203
222
  end
204
223
  end
205
224
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: berater
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-07 00:00:00.000000000 Z
11
+ date: 2021-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis