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 +4 -4
- data/lib/berater/concurrency_limiter.rb +4 -15
- data/lib/berater/inhibitor.rb +3 -6
- data/lib/berater/limiter.rb +29 -15
- data/lib/berater/rate_limiter.rb +2 -10
- data/lib/berater/rspec/matchers.rb +18 -20
- data/lib/berater/test_mode.rb +20 -29
- data/lib/berater/unlimiter.rb +5 -7
- data/lib/berater/version.rb +1 -1
- data/spec/concurrency_limiter_spec.rb +18 -8
- data/spec/test_mode_spec.rb +42 -23
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c5cc50ba02c18bf9ef9f6b9c1dd6ccb8f8af0297df95757ac13406dc4539c40
|
4
|
+
data.tar.gz: f80341dccff30ca3f344f3bdf21de44b1ef1715bfaf4140933a91252ddb7231e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ada3fffc841e71498f652dc990eed482bccc942b86280fc5ee736d49fdf99d8ab2f921b5b52d96f3588c268c34343669c00e0be8822c462a3e6f9dcf954a4005
|
7
|
+
data.tar.gz: 0363abef69cfdad8140d89373b5088cfa8a6259f16716011c82ceb1e2e9e948cdb4d44ddd36aaf1d05b121dc7d275d0ee34571a0a3ee9b964f4286375ec356cf
|
@@ -63,15 +63,10 @@ module Berater
|
|
63
63
|
LUA
|
64
64
|
)
|
65
65
|
|
66
|
-
def
|
67
|
-
|
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
|
-
|
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)
|
data/lib/berater/inhibitor.rb
CHANGED
@@ -7,14 +7,11 @@ module Berater
|
|
7
7
|
super(key, 0, **opts)
|
8
8
|
end
|
9
9
|
|
10
|
-
|
11
|
-
raise Inhibited
|
12
|
-
end
|
10
|
+
alias inhibited? overloaded?
|
13
11
|
|
14
|
-
def
|
15
|
-
|
12
|
+
protected def acquire_lock(*)
|
13
|
+
raise Inhibited
|
16
14
|
end
|
17
|
-
alias inhibited? overloaded?
|
18
15
|
|
19
16
|
end
|
20
17
|
end
|
data/lib/berater/limiter.rb
CHANGED
@@ -7,12 +7,34 @@ module Berater
|
|
7
7
|
options[:redis] || Berater.redis
|
8
8
|
end
|
9
9
|
|
10
|
-
def limit
|
11
|
-
|
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
|
-
|
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
|
64
|
-
|
85
|
+
def acquire_lock(capacity, cost)
|
86
|
+
raise NotImplementedError
|
65
87
|
end
|
66
88
|
|
67
|
-
def
|
68
|
-
|
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
|
data/lib/berater/rate_limiter.rb
CHANGED
@@ -63,9 +63,7 @@ module Berater
|
|
63
63
|
LUA
|
64
64
|
)
|
65
65
|
|
66
|
-
def
|
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
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
res = obj.call
|
13
|
+
case obj
|
14
|
+
when Proc
|
15
|
+
# eg. expect { ... }.to be_overrated
|
16
|
+
res = obj.call
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
34
|
-
|
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
|
data/lib/berater/test_mode.rb
CHANGED
@@ -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
|
23
|
-
|
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
|
-
|
20
|
+
Lock.new(Float::INFINITY, 0)
|
29
21
|
when :fail
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
data/lib/berater/unlimiter.rb
CHANGED
@@ -5,16 +5,14 @@ module Berater
|
|
5
5
|
super(key, Float::INFINITY, **opts)
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
yield_lock(Lock.new(Float::INFINITY, 0), &block)
|
10
|
-
end
|
8
|
+
protected
|
11
9
|
|
12
|
-
def
|
13
|
-
|
10
|
+
def capacity=(*)
|
11
|
+
@capacity = Float::INFINITY
|
14
12
|
end
|
15
13
|
|
16
|
-
|
17
|
-
|
14
|
+
def acquire_lock(*)
|
15
|
+
Lock.new(Float::INFINITY, 0)
|
18
16
|
end
|
19
17
|
|
20
18
|
end
|
data/lib/berater/version.rb
CHANGED
@@ -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
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
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
|
|
data/spec/test_mode_spec.rb
CHANGED
@@ -1,31 +1,20 @@
|
|
1
|
-
describe Berater::TestMode
|
2
|
-
let(:reset_test_mode) { true }
|
3
|
-
|
1
|
+
describe Berater::TestMode do
|
4
2
|
after do
|
5
|
-
Berater.test_mode = nil
|
3
|
+
Berater.test_mode = nil
|
6
4
|
end
|
7
5
|
|
8
|
-
context 'after test_mode.rb
|
9
|
-
|
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 '
|
22
|
-
expect(Berater::
|
23
|
-
|
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.
|
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-
|
11
|
+
date: 2021-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|