berater 0.10.1 → 0.11.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.
- checksums.yaml +4 -4
- data/lib/berater/limiter.rb +5 -5
- 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/version.rb +1 -1
- data/lib/berater.rb +1 -0
- data/spec/middleware/fail_open_spec.rb +184 -0
- data/spec/middleware/load_shedder_spec.rb +130 -0
- data/spec/middleware_spec.rb +10 -0
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f57ab651eeb34e6a0bb7b889fefa9c13d1d1fc5b6e834bc2b9a6e0ba7b74a8e2
|
4
|
+
data.tar.gz: 8112aa91ae48872132d8222a07f03dce42ff6f5ff26a3bf17906a389ba675ec9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17ae5da54dcf9535d4afe93d7e239ddc1fefaedf3419883cfa22d1f55c0c0aa7a9365986509e8e54e37c08c258068db4a76064dcc6af3f654e170b3efc8c1cd0
|
7
|
+
data.tar.gz: 04b1e7553e86107bbc91fc486b7b0ae099f5575b8654f7800f2a1c281b7b8bfde1e63df2378f03fcb4a9864b15b6a5de610bb6e7a1ce4d4b8e53898d9f4f0869
|
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?
|
@@ -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/version.rb
CHANGED
data/lib/berater.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
@@ -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
|
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.0
|
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
|
@@ -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.1.
|
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
|