berater 0.9.0 → 0.11.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/limiter.rb +34 -7
- data/lib/berater/lock.rb +1 -1
- data/lib/berater/lua_script.rb +4 -3
- data/lib/berater/middleware/fail_open.rb +41 -0
- data/lib/berater/middleware/load_shedder.rb +31 -0
- data/lib/berater/middleware.rb +6 -0
- data/lib/berater/utils.rb +1 -6
- data/lib/berater/version.rb +1 -1
- data/lib/berater.rb +1 -0
- data/spec/berater_spec.rb +2 -2
- data/spec/concurrency_limiter_spec.rb +2 -1
- data/spec/limiter_spec.rb +48 -0
- data/spec/lua_script_spec.rb +0 -1
- data/spec/middleware/fail_open_spec.rb +184 -0
- data/spec/middleware/load_shedder_spec.rb +130 -0
- data/spec/middleware_spec.rb +17 -7
- data/spec/rate_limiter_spec.rb +3 -2
- metadata +14 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b657c7eb3a69e868416ad2d149d6c35ab13989e77a16ece23217deaaa9fafb6
|
4
|
+
data.tar.gz: 30835e7c247da37543ea118067d0b61d16080399ff6b53e86783c53a94b8d560
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '04481aa9c3097930f66eb29c6e19e650309b882cf835c1f36cb3fa58524b957d1194b5acc6a83f9fdfd33b73f888261ff3cc3171d730fd9781d9cf768b2b0768'
|
7
|
+
data.tar.gz: e0cd1a220a418face72685b44d23dff0040dc5f3d94b8c0d82769123a1c387e709702e8f093cc9c0f600d002ea4c848ebd4f0a2e38a4368e743104fbee597f9d
|
data/lib/berater/limiter.rb
CHANGED
@@ -7,12 +7,12 @@ module Berater
|
|
7
7
|
options[:redis] || Berater.redis
|
8
8
|
end
|
9
9
|
|
10
|
-
def limit(
|
11
|
-
capacity ||= @capacity
|
12
|
-
|
10
|
+
def limit(**opts, &block)
|
11
|
+
opts[:capacity] ||= @capacity
|
12
|
+
opts[:cost] ||= 1
|
13
13
|
|
14
|
-
Berater.middleware.call(self,
|
15
|
-
|
14
|
+
lock = Berater.middleware.call(self, **opts) do |limiter, **opts|
|
15
|
+
limiter.inner_limit(**opts)
|
16
16
|
end
|
17
17
|
|
18
18
|
if block_given?
|
@@ -27,10 +27,24 @@ module Berater
|
|
27
27
|
end
|
28
28
|
|
29
29
|
protected def inner_limit(capacity:, cost:)
|
30
|
+
if capacity.is_a?(String)
|
31
|
+
# try casting
|
32
|
+
begin
|
33
|
+
capacity = Float(capacity)
|
34
|
+
rescue ArgumentError; end
|
35
|
+
end
|
36
|
+
|
30
37
|
unless capacity.is_a?(Numeric) && capacity >= 0
|
31
38
|
raise ArgumentError, "invalid capacity: #{capacity}"
|
32
39
|
end
|
33
40
|
|
41
|
+
if cost.is_a?(String)
|
42
|
+
# try casting
|
43
|
+
begin
|
44
|
+
cost = Float(cost)
|
45
|
+
rescue ArgumentError; end
|
46
|
+
end
|
47
|
+
|
34
48
|
unless cost.is_a?(Numeric) && cost >= 0 && cost < Float::INFINITY
|
35
49
|
raise ArgumentError, "invalid cost: #{cost}"
|
36
50
|
end
|
@@ -76,6 +90,13 @@ module Berater
|
|
76
90
|
end
|
77
91
|
|
78
92
|
def capacity=(capacity)
|
93
|
+
if capacity.is_a?(String)
|
94
|
+
# try casting
|
95
|
+
begin
|
96
|
+
capacity = Float(capacity)
|
97
|
+
rescue TypeError, ArgumentError; end
|
98
|
+
end
|
99
|
+
|
79
100
|
unless capacity.is_a?(Numeric)
|
80
101
|
raise ArgumentError, "expected Numeric, found #{capacity.class}"
|
81
102
|
end
|
@@ -99,11 +120,16 @@ module Berater
|
|
99
120
|
end
|
100
121
|
|
101
122
|
class << self
|
102
|
-
def new(*)
|
123
|
+
def new(*args, **kwargs)
|
103
124
|
# can only call via subclass
|
104
125
|
raise NoMethodError if self == Berater::Limiter
|
105
126
|
|
106
|
-
|
127
|
+
if RUBY_VERSION < '3' && kwargs.empty?
|
128
|
+
# avoid ruby 2 problems with empty hashes
|
129
|
+
super(*args)
|
130
|
+
else
|
131
|
+
super
|
132
|
+
end
|
107
133
|
end
|
108
134
|
|
109
135
|
def cache_key(key)
|
@@ -116,6 +142,7 @@ module Berater
|
|
116
142
|
def inherited(subclass)
|
117
143
|
# automagically create convenience method
|
118
144
|
name = subclass.to_s.split(':')[-1]
|
145
|
+
|
119
146
|
Berater.define_singleton_method(name) do |*args, **opts, &block|
|
120
147
|
Berater::Utils.convenience_fn(subclass, *args, **opts, &block)
|
121
148
|
end
|
data/lib/berater/lock.rb
CHANGED
data/lib/berater/lua_script.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'digest'
|
2
|
+
require 'redis'
|
2
3
|
|
3
4
|
module Berater
|
4
5
|
class LuaScript
|
@@ -6,11 +7,11 @@ module Berater
|
|
6
7
|
attr_reader :source
|
7
8
|
|
8
9
|
def initialize(source)
|
9
|
-
@source = source
|
10
|
+
@source = source.dup.freeze
|
10
11
|
end
|
11
12
|
|
12
13
|
def sha
|
13
|
-
@sha ||= Digest::SHA1.hexdigest(minify)
|
14
|
+
@sha ||= Digest::SHA1.hexdigest(minify).freeze
|
14
15
|
end
|
15
16
|
|
16
17
|
def eval(redis, *args)
|
@@ -44,7 +45,7 @@ module Berater
|
|
44
45
|
def minify
|
45
46
|
# trim comments (whole line and partial)
|
46
47
|
# and whitespace (prefix and empty lines)
|
47
|
-
@minify ||= source.gsub(/^\s*--.*\n|\s*--.*|^\s*|^$\n/, '').chomp
|
48
|
+
@minify ||= source.gsub(/^\s*--.*\n|\s*--.*|^\s*|^$\n/, '').chomp.freeze
|
48
49
|
end
|
49
50
|
|
50
51
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Berater
|
4
|
+
module Middleware
|
5
|
+
class FailOpen
|
6
|
+
ERRORS = Set[
|
7
|
+
Redis::BaseConnectionError,
|
8
|
+
]
|
9
|
+
|
10
|
+
def initialize(errors: nil, on_fail: nil)
|
11
|
+
@errors = errors || ERRORS
|
12
|
+
@on_fail = on_fail
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(*, **opts)
|
16
|
+
yield.tap do |lock|
|
17
|
+
# wrap lock.release so it fails open
|
18
|
+
|
19
|
+
# save reference to original function
|
20
|
+
release_fn = lock.method(:release)
|
21
|
+
|
22
|
+
# make bound variables accessible to block
|
23
|
+
errors = @errors
|
24
|
+
on_fail = @on_fail
|
25
|
+
|
26
|
+
lock.define_singleton_method(:release) do
|
27
|
+
release_fn.call
|
28
|
+
rescue *errors => e
|
29
|
+
on_fail&.call(e)
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
rescue *@errors => e
|
34
|
+
@on_fail&.call(e)
|
35
|
+
|
36
|
+
# fail open by faking a lock
|
37
|
+
Berater::Lock.new(opts[:capacity], -1)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Berater
|
2
|
+
module Middleware
|
3
|
+
class LoadShedder
|
4
|
+
PRIORITY_RANGE = 1..5
|
5
|
+
|
6
|
+
def initialize(default_priority: nil)
|
7
|
+
@default_priority = default_priority
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(*args, **opts)
|
11
|
+
if priority = opts.delete(:priority) || @default_priority
|
12
|
+
opts[:capacity] = adjust_capacity(opts[:capacity], priority)
|
13
|
+
end
|
14
|
+
|
15
|
+
yield *args, **opts
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def adjust_capacity(capacity, priority)
|
21
|
+
unless PRIORITY_RANGE.include?(priority)
|
22
|
+
return capacity
|
23
|
+
end
|
24
|
+
|
25
|
+
# priority 1 stays at 100%, 2 scales down to 90%, 5 to 60%
|
26
|
+
factor = 1 - (priority - 1) * 0.1
|
27
|
+
(capacity * factor).floor
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/berater/utils.rb
CHANGED
data/lib/berater/version.rb
CHANGED
data/lib/berater.rb
CHANGED
data/spec/berater_spec.rb
CHANGED
@@ -59,8 +59,8 @@ describe Berater do
|
|
59
59
|
end
|
60
60
|
|
61
61
|
it 'accepts an optional redis parameter' do
|
62
|
-
redis = double(
|
63
|
-
limiter = Berater.new(:key, capacity, opts.merge(redis: redis))
|
62
|
+
redis = double(Redis)
|
63
|
+
limiter = Berater.new(:key, capacity, **opts.merge(redis: redis))
|
64
64
|
expect(limiter.redis).to be redis
|
65
65
|
end
|
66
66
|
end
|
@@ -24,6 +24,7 @@ describe Berater::ConcurrencyLimiter do
|
|
24
24
|
it { expect_capacity(0) }
|
25
25
|
it { expect_capacity(1) }
|
26
26
|
it { expect_capacity(1.5) }
|
27
|
+
it { expect_capacity('1.5') }
|
27
28
|
it { expect_capacity(10_000) }
|
28
29
|
|
29
30
|
context 'with erroneous values' do
|
@@ -34,7 +35,7 @@ describe Berater::ConcurrencyLimiter do
|
|
34
35
|
end
|
35
36
|
|
36
37
|
it { expect_bad_capacity(-1) }
|
37
|
-
it { expect_bad_capacity('
|
38
|
+
it { expect_bad_capacity('abc') }
|
38
39
|
it { expect_bad_capacity(:one) }
|
39
40
|
it { expect_bad_capacity(Float::INFINITY) }
|
40
41
|
end
|
data/spec/limiter_spec.rb
CHANGED
@@ -14,6 +14,32 @@ describe Berater::Limiter do
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
describe '#capacity=' do
|
18
|
+
subject { Berater::Unlimiter.new(:key, capacity).capacity }
|
19
|
+
|
20
|
+
context 'when capacity is numeric' do
|
21
|
+
let(:capacity) { 3.5 }
|
22
|
+
|
23
|
+
it { is_expected.to be capacity }
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'when capacity is a stringified numeric' do
|
27
|
+
let(:capacity) { '3.5' }
|
28
|
+
|
29
|
+
it 'casts the value gracefully' do
|
30
|
+
is_expected.to be capacity.to_f
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'when capacity is a bogus value' do
|
35
|
+
let(:capacity) { :abc }
|
36
|
+
|
37
|
+
it 'raises' do
|
38
|
+
expect { subject }.to raise_error(ArgumentError)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
17
43
|
describe '#limit' do
|
18
44
|
subject { Berater::Unlimiter.new }
|
19
45
|
|
@@ -29,6 +55,12 @@ describe Berater::Limiter do
|
|
29
55
|
subject.limit(capacity: 'abc')
|
30
56
|
}.to raise_error(ArgumentError)
|
31
57
|
end
|
58
|
+
|
59
|
+
it 'handles stringified numerics gracefully' do
|
60
|
+
is_expected.to receive(:acquire_lock).with(3.5, anything)
|
61
|
+
|
62
|
+
subject.limit(capacity: '3.5')
|
63
|
+
end
|
32
64
|
end
|
33
65
|
|
34
66
|
context 'with a cost parameter' do
|
@@ -51,6 +83,12 @@ describe Berater::Limiter do
|
|
51
83
|
subject.limit(cost: Float::INFINITY)
|
52
84
|
}.to raise_error(ArgumentError)
|
53
85
|
end
|
86
|
+
|
87
|
+
it 'handles stringified numerics gracefully' do
|
88
|
+
is_expected.to receive(:acquire_lock).with(anything, 2.5)
|
89
|
+
|
90
|
+
subject.limit(cost: '2.5')
|
91
|
+
end
|
54
92
|
end
|
55
93
|
|
56
94
|
context 'when Berater.redis is nil' do
|
@@ -79,6 +117,16 @@ describe Berater::Limiter do
|
|
79
117
|
expect { limiter.limit }.to raise_error(RuntimeError)
|
80
118
|
end
|
81
119
|
end
|
120
|
+
|
121
|
+
it 'releases the lock even when limited code raises an error' do
|
122
|
+
lock = Berater::Lock.new(Float::INFINITY, 0)
|
123
|
+
expect(subject).to receive(:acquire_lock).and_return(lock)
|
124
|
+
expect(lock).to receive(:release)
|
125
|
+
|
126
|
+
expect {
|
127
|
+
subject.limit { raise 'fail' }
|
128
|
+
}.to raise_error(RuntimeError)
|
129
|
+
end
|
82
130
|
end
|
83
131
|
|
84
132
|
describe '#==' do
|
data/spec/lua_script_spec.rb
CHANGED
@@ -0,0 +1,184 @@
|
|
1
|
+
describe Berater::Middleware::FailOpen do
|
2
|
+
let(:limiter) { Berater::Unlimiter.new }
|
3
|
+
let(:lock) { limiter.limit }
|
4
|
+
let(:error) { Redis::TimeoutError }
|
5
|
+
|
6
|
+
describe '.call' do
|
7
|
+
let(:instance) { described_class.new(errors: errors, on_fail: on_fail) }
|
8
|
+
let(:errors) { nil }
|
9
|
+
let(:on_fail) { nil }
|
10
|
+
|
11
|
+
it 'returns the blocks value' do
|
12
|
+
expect(instance.call { lock }).to be lock
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'when there is an error during lock acquisition' do
|
16
|
+
subject { instance.call { raise error } }
|
17
|
+
|
18
|
+
it 'still returns a lock' do
|
19
|
+
expect(subject).to be_a Berater::Lock
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'creates a new, fake lock' do
|
23
|
+
expect(Berater::Lock).to receive(:new)
|
24
|
+
subject
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns a lock that is releasable' do
|
28
|
+
expect(subject.release).to be true
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when an on_fail handler is defined' do
|
32
|
+
let(:on_fail) { double(Proc) }
|
33
|
+
|
34
|
+
it 'calls the handler' do
|
35
|
+
expect(on_fail).to receive(:call).with(error)
|
36
|
+
subject
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when the error is an IOError' do
|
41
|
+
let(:error) { IOError }
|
42
|
+
|
43
|
+
it 'would normally not catch the error' do
|
44
|
+
expect { subject }.to raise_error(error)
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'and errors option is set' do
|
48
|
+
let(:errors) { [ error ] }
|
49
|
+
|
50
|
+
it 'catches the error' do
|
51
|
+
expect { subject }.not_to raise_error
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when there is an error during lock release' do
|
58
|
+
subject { instance.call { lock }.release }
|
59
|
+
|
60
|
+
before do
|
61
|
+
expect(lock).to receive(:release).and_raise(error)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'handles the exception' do
|
65
|
+
expect { subject }.not_to raise_error
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'returns false since lock was not released' do
|
69
|
+
is_expected.to be false
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'when an on_fail handler is defined' do
|
73
|
+
let(:on_fail) { double(Proc) }
|
74
|
+
|
75
|
+
it 'calls the handler' do
|
76
|
+
expect(on_fail).to receive(:call).with(Exception)
|
77
|
+
subject
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when the error is an IOError' do
|
82
|
+
let(:error) { IOError }
|
83
|
+
|
84
|
+
it 'would normally not catch the error' do
|
85
|
+
expect { subject }.to raise_error(error)
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'and errors option is set' do
|
89
|
+
let(:errors) { [ error ] }
|
90
|
+
|
91
|
+
it 'catches the error' do
|
92
|
+
expect { subject }.not_to raise_error
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'when there is an error during lock acquisition' do
|
100
|
+
before do
|
101
|
+
expect(limiter).to receive(:acquire_lock).and_raise(error)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'raises an exception for the caller' do
|
105
|
+
expect { limiter.limit }.to raise_error(error)
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'when FailOpen middleware is enabled' do
|
109
|
+
before do
|
110
|
+
Berater.middleware.use described_class
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'fails open' do
|
114
|
+
expect(limiter.limit).to be_a Berater::Lock
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'returns the intended result' do
|
118
|
+
expect(limiter.limit { 123 }).to be 123
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'when FailOpen middleware is enabled with callback' do
|
123
|
+
before do
|
124
|
+
Berater.middleware.use described_class, on_fail: on_fail
|
125
|
+
end
|
126
|
+
let(:on_fail) { double(Proc) }
|
127
|
+
|
128
|
+
it 'calls the callback' do
|
129
|
+
expect(on_fail).to receive(:call).with(Exception)
|
130
|
+
limiter.limit
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'when there is an error during lock release' do
|
136
|
+
before do
|
137
|
+
allow(limiter).to receive(:acquire_lock).and_return(lock)
|
138
|
+
allow(lock).to receive(:release).and_raise(error)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'acquires a lock' do
|
142
|
+
expect(limiter.limit).to be_a Berater::Lock
|
143
|
+
expect(limiter.limit).to be lock
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'raises an exception when lock is released' do
|
147
|
+
expect {
|
148
|
+
limiter.limit.release
|
149
|
+
}.to raise_error(error)
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'raises an exception when lock is auto released' do
|
153
|
+
expect {
|
154
|
+
limiter.limit {}
|
155
|
+
}.to raise_error(error)
|
156
|
+
end
|
157
|
+
|
158
|
+
context 'when FailOpen middleware is enabled' do
|
159
|
+
before do
|
160
|
+
Berater.middleware.use described_class
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'fails open' do
|
164
|
+
expect { limiter.limit.release }.not_to raise_error
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'returns the intended result' do
|
168
|
+
expect(limiter.limit { 123 }).to be 123
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'when FailOpen middleware is enabled with callback' do
|
173
|
+
before do
|
174
|
+
Berater.middleware.use described_class, on_fail: on_fail
|
175
|
+
end
|
176
|
+
let(:on_fail) { double(Proc) }
|
177
|
+
|
178
|
+
it 'calls the callback' do
|
179
|
+
expect(on_fail).to receive(:call).with(Exception)
|
180
|
+
limiter.limit {}
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
describe Berater::Middleware::LoadShedder do
|
2
|
+
describe '#call' do
|
3
|
+
subject { described_class.new }
|
4
|
+
|
5
|
+
before { Berater.test_mode = :pass }
|
6
|
+
|
7
|
+
it 'yields' do
|
8
|
+
expect {|b| subject.call(&b) }.to yield_control
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'passes through capacity and cost options' do
|
12
|
+
opts = {
|
13
|
+
capacity: 1,
|
14
|
+
cost: 2,
|
15
|
+
}
|
16
|
+
|
17
|
+
subject.call(**opts) do |**passed_opts|
|
18
|
+
expect(passed_opts).to eq(opts)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'strips out priority from options' do
|
23
|
+
opts = {
|
24
|
+
capacity: 1,
|
25
|
+
priority: 3,
|
26
|
+
}
|
27
|
+
|
28
|
+
subject.call(**opts) do |**passed_opts|
|
29
|
+
expect(passed_opts.keys).not_to include(:priority)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'keeps full capacity for priority 1' do
|
34
|
+
subject.call(capacity: 100, priority: 1) do |capacity:|
|
35
|
+
expect(capacity).to eq 100
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'adjusts the capactiy according to priority' do
|
40
|
+
subject.call(capacity: 100, priority: 2) do |capacity:|
|
41
|
+
expect(capacity).to be < 100
|
42
|
+
end
|
43
|
+
|
44
|
+
subject.call(capacity: 100, priority: 5) do |capacity:|
|
45
|
+
expect(capacity).to eq 60
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'ignores bogus priority options' do
|
50
|
+
subject.call(capacity: 100, priority: 50) do |capacity:|
|
51
|
+
expect(capacity).to eq 100
|
52
|
+
end
|
53
|
+
|
54
|
+
subject.call(capacity: 100, priority: 'abc') do |capacity:|
|
55
|
+
expect(capacity).to eq 100
|
56
|
+
end
|
57
|
+
|
58
|
+
subject.call(capacity: 100, priority: '123') do |capacity:|
|
59
|
+
expect(capacity).to eq 100
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'works with a fractional priority' do
|
64
|
+
subject.call(capacity: 100, priority: 1.5) do |capacity:|
|
65
|
+
expect(capacity).to be < 100
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'with a default priority' do
|
70
|
+
subject { described_class.new(default_priority: 5) }
|
71
|
+
|
72
|
+
it 'keeps full capacity for priority 1' do
|
73
|
+
subject.call(capacity: 100, priority: 1) do |capacity:|
|
74
|
+
expect(capacity).to eq 100
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'uses the default priority' do
|
79
|
+
subject.call(capacity: 100) do |capacity:|
|
80
|
+
expect(capacity).to eq 60
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'with a limiter' do
|
87
|
+
before do
|
88
|
+
Berater.middleware.use Berater::Middleware::LoadShedder
|
89
|
+
end
|
90
|
+
|
91
|
+
shared_examples 'limiter load shedding' do |limiter|
|
92
|
+
it 'passes through the capactiy properly' do
|
93
|
+
expect(limiter).to receive(:inner_limit).with(
|
94
|
+
hash_including(capacity: 100)
|
95
|
+
).and_call_original
|
96
|
+
|
97
|
+
limiter.limit
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'scales the capactiy with priority' do
|
101
|
+
expect(limiter).to receive(:inner_limit).with(
|
102
|
+
hash_including(capacity: 60)
|
103
|
+
).and_call_original
|
104
|
+
|
105
|
+
limiter.limit(priority: 5)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'overloads properly' do
|
109
|
+
60.times { limiter.limit(priority: 5) }
|
110
|
+
|
111
|
+
expect {
|
112
|
+
limiter.limit(priority: 5)
|
113
|
+
}.to be_overloaded
|
114
|
+
|
115
|
+
expect {
|
116
|
+
limiter.limit(priority: 4)
|
117
|
+
}.not_to be_overloaded
|
118
|
+
|
119
|
+
39.times { limiter.limit(priority: 1) }
|
120
|
+
|
121
|
+
expect {
|
122
|
+
limiter.limit(priority: 1)
|
123
|
+
}.to be_overloaded
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
include_examples 'limiter load shedding', Berater::ConcurrencyLimiter.new(:key, 100)
|
128
|
+
include_examples 'limiter load shedding', Berater::RateLimiter.new(:key, 100, :second)
|
129
|
+
end
|
130
|
+
end
|
data/spec/middleware_spec.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
class Meddler
|
2
|
-
def call(*)
|
2
|
+
def call(*args, **kwargs)
|
3
3
|
yield
|
4
4
|
end
|
5
5
|
end
|
@@ -73,18 +73,18 @@ describe 'Berater.middleware' do
|
|
73
73
|
|
74
74
|
context 'when middleware meddles' do
|
75
75
|
it 'can change the capacity' do
|
76
|
-
expect(middleware).to receive(:call) do |limiter, opts, &block|
|
76
|
+
expect(middleware).to receive(:call) do |limiter, **opts, &block|
|
77
77
|
opts[:capacity] = 0
|
78
|
-
block.call
|
78
|
+
block.call(limiter, **opts)
|
79
79
|
end
|
80
80
|
|
81
81
|
expect { limiter.limit }.to be_overloaded
|
82
82
|
end
|
83
83
|
|
84
84
|
it 'can change the cost' do
|
85
|
-
expect(middleware).to receive(:call) do |limiter, opts, &block|
|
85
|
+
expect(middleware).to receive(:call) do |limiter, **opts, &block|
|
86
86
|
opts[:cost] = 2
|
87
|
-
block.call
|
87
|
+
block.call(limiter, **opts)
|
88
88
|
end
|
89
89
|
|
90
90
|
expect { limiter.limit }.to be_overloaded
|
@@ -93,8 +93,8 @@ describe 'Berater.middleware' do
|
|
93
93
|
it 'can change the limiter' do
|
94
94
|
other_limiter = Berater::Inhibitor.new
|
95
95
|
|
96
|
-
expect(middleware).to receive(:call) do |limiter, opts, &block|
|
97
|
-
block.call
|
96
|
+
expect(middleware).to receive(:call) do |limiter, **opts, &block|
|
97
|
+
block.call(other_limiter, **opts)
|
98
98
|
end
|
99
99
|
expect(other_limiter).to receive(:acquire_lock).and_call_original
|
100
100
|
|
@@ -105,6 +105,16 @@ describe 'Berater.middleware' do
|
|
105
105
|
expect(middleware).to receive(:call)
|
106
106
|
expect(limiter.limit).to be nil
|
107
107
|
end
|
108
|
+
|
109
|
+
it 'can intercept the lock' do
|
110
|
+
expect(middleware).to receive(:call) do |&block|
|
111
|
+
lock = block.call
|
112
|
+
expect(lock).to be_a Berater::Lock
|
113
|
+
expect(lock.capacity).to eq limiter.capacity
|
114
|
+
end
|
115
|
+
|
116
|
+
limiter.limit
|
117
|
+
end
|
108
118
|
end
|
109
119
|
end
|
110
120
|
end
|
data/spec/rate_limiter_spec.rb
CHANGED
@@ -19,12 +19,13 @@ describe Berater::RateLimiter do
|
|
19
19
|
describe '#capacity' do
|
20
20
|
def expect_capacity(capacity)
|
21
21
|
limiter = described_class.new(:key, capacity, :second)
|
22
|
-
expect(limiter.capacity).to eq capacity
|
22
|
+
expect(limiter.capacity).to eq capacity.to_f
|
23
23
|
end
|
24
24
|
|
25
25
|
it { expect_capacity(0) }
|
26
26
|
it { expect_capacity(1) }
|
27
27
|
it { expect_capacity(1.5) }
|
28
|
+
it { expect_capacity('1.5') }
|
28
29
|
it { expect_capacity(100) }
|
29
30
|
|
30
31
|
context 'with erroneous values' do
|
@@ -35,7 +36,7 @@ describe Berater::RateLimiter do
|
|
35
36
|
end
|
36
37
|
|
37
38
|
it { expect_bad_capacity(-1) }
|
38
|
-
it { expect_bad_capacity('
|
39
|
+
it { expect_bad_capacity('abc') }
|
39
40
|
it { expect_bad_capacity(:one) }
|
40
41
|
it { expect_bad_capacity(Float::INFINITY) }
|
41
42
|
end
|
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.
|
4
|
+
version: 0.11.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-
|
11
|
+
date: 2021-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: meddleware
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
19
|
+
version: '0.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
26
|
+
version: '0.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: redis
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '3'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '3'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: benchmark
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,6 +150,9 @@ files:
|
|
150
150
|
- lib/berater/limiter_set.rb
|
151
151
|
- lib/berater/lock.rb
|
152
152
|
- lib/berater/lua_script.rb
|
153
|
+
- lib/berater/middleware.rb
|
154
|
+
- lib/berater/middleware/fail_open.rb
|
155
|
+
- lib/berater/middleware/load_shedder.rb
|
153
156
|
- lib/berater/rate_limiter.rb
|
154
157
|
- lib/berater/rspec.rb
|
155
158
|
- lib/berater/rspec/matchers.rb
|
@@ -167,6 +170,8 @@ files:
|
|
167
170
|
- spec/limiter_spec.rb
|
168
171
|
- spec/lua_script_spec.rb
|
169
172
|
- spec/matchers_spec.rb
|
173
|
+
- spec/middleware/fail_open_spec.rb
|
174
|
+
- spec/middleware/load_shedder_spec.rb
|
170
175
|
- spec/middleware_spec.rb
|
171
176
|
- spec/rate_limiter_spec.rb
|
172
177
|
- spec/riddle_spec.rb
|
@@ -193,12 +198,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
193
198
|
- !ruby/object:Gem::Version
|
194
199
|
version: '0'
|
195
200
|
requirements: []
|
196
|
-
rubygems_version: 3.
|
201
|
+
rubygems_version: 3.1.6
|
197
202
|
signing_key:
|
198
203
|
specification_version: 4
|
199
204
|
summary: Berater
|
200
205
|
test_files:
|
201
206
|
- spec/rate_limiter_spec.rb
|
207
|
+
- spec/middleware/load_shedder_spec.rb
|
208
|
+
- spec/middleware/fail_open_spec.rb
|
202
209
|
- spec/matchers_spec.rb
|
203
210
|
- spec/dsl_refinement_spec.rb
|
204
211
|
- spec/test_mode_spec.rb
|