berater 0.11.1 → 0.12.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/middleware/fail_open.rb +2 -0
- data/lib/berater/middleware/load_shedder.rb +5 -0
- data/lib/berater/middleware/statsd.rb +82 -0
- data/lib/berater/middleware.rb +1 -0
- data/lib/berater/version.rb +1 -1
- data/spec/limiter_spec.rb +4 -2
- data/spec/middleware/fail_open_spec.rb +12 -0
- data/spec/middleware/load_shedder_spec.rb +36 -16
- data/spec/middleware/statsd_spec.rb +227 -0
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72cc5fcf9c68fff6dd3988ff85897b38d6fdcff21c0e6f94d3ec098ca8b35fc7
|
4
|
+
data.tar.gz: 49144f0dee2d7197740d39b9da365f9d959c730b7ece1ae761948ec9263ada75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 401a193c358a80098f1250053983d8f1a362cd14649156a036deedeb461fd70202743764d375b04a2b6b4fb08c795ec61d013e1be342626a2c8aad7dab2cb259
|
7
|
+
data.tar.gz: 55498f8b1a6bd70a64ed9053b80bc9c95b0d60043f073c302d48bb79b51677d7a3c235c07f35cc51dacec067d0c2cc8dcf972e37fb9e9baae126f69fa8aacb4f
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Berater
|
2
|
+
module Middleware
|
3
|
+
class Statsd
|
4
|
+
def initialize(client, tags: {})
|
5
|
+
@client = client
|
6
|
+
@tags = tags
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(limiter, **opts)
|
10
|
+
duration = -Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
11
|
+
lock = yield
|
12
|
+
rescue Exception => error
|
13
|
+
# note exception and propagate
|
14
|
+
raise
|
15
|
+
ensure
|
16
|
+
duration += Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
17
|
+
duration = (duration * 1_000).round(2) # milliseconds
|
18
|
+
|
19
|
+
tags = build_tags(limiter, opts)
|
20
|
+
|
21
|
+
@client.timing(
|
22
|
+
'berater.limiter.limit',
|
23
|
+
duration,
|
24
|
+
tags: tags.merge(overloaded: !lock),
|
25
|
+
)
|
26
|
+
|
27
|
+
@client.gauge(
|
28
|
+
'berater.limiter.capacity',
|
29
|
+
limiter.capacity,
|
30
|
+
tags: tags,
|
31
|
+
)
|
32
|
+
|
33
|
+
if lock && lock.contention > 0 # not a failsafe lock
|
34
|
+
@client.gauge(
|
35
|
+
'berater.lock.capacity',
|
36
|
+
lock.capacity,
|
37
|
+
tags: tags,
|
38
|
+
)
|
39
|
+
@client.gauge(
|
40
|
+
'berater.limiter.contention',
|
41
|
+
lock.contention,
|
42
|
+
tags: tags,
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
if error
|
47
|
+
if error.is_a?(Berater::Overloaded)
|
48
|
+
# overloaded, so contention >= capacity
|
49
|
+
@client.gauge(
|
50
|
+
'berater.limiter.contention',
|
51
|
+
limiter.capacity,
|
52
|
+
tags: tags,
|
53
|
+
)
|
54
|
+
else
|
55
|
+
@client.increment(
|
56
|
+
'berater.limiter.error',
|
57
|
+
tags: tags.merge(type: error.class.to_s)
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def build_tags(limiter, opts)
|
66
|
+
tags = {
|
67
|
+
key: limiter.key,
|
68
|
+
limiter: limiter.class.to_s.split(':')[-1],
|
69
|
+
}
|
70
|
+
|
71
|
+
# append custom tags
|
72
|
+
if @tags.respond_to?(:call)
|
73
|
+
tags.merge!(@tags.call(limiter, **opts) || {})
|
74
|
+
else
|
75
|
+
tags.merge!(@tags)
|
76
|
+
end
|
77
|
+
|
78
|
+
tags.merge!(opts.fetch(:tags, {}))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/berater/middleware.rb
CHANGED
data/lib/berater/version.rb
CHANGED
data/spec/limiter_spec.rb
CHANGED
@@ -15,7 +15,9 @@ describe Berater::Limiter do
|
|
15
15
|
end
|
16
16
|
|
17
17
|
describe '#capacity=' do
|
18
|
-
subject
|
18
|
+
subject do
|
19
|
+
Berater::RateLimiter.new(:key, capacity, :second).capacity
|
20
|
+
end
|
19
21
|
|
20
22
|
context 'when capacity is numeric' do
|
21
23
|
let(:capacity) { 3.5 }
|
@@ -26,7 +28,7 @@ describe Berater::Limiter do
|
|
26
28
|
context 'when capacity is a stringified numeric' do
|
27
29
|
let(:capacity) { '3.5' }
|
28
30
|
|
29
|
-
it 'casts the value
|
31
|
+
it 'casts the value' do
|
30
32
|
is_expected.to be capacity.to_f
|
31
33
|
end
|
32
34
|
end
|
@@ -94,6 +94,18 @@ describe Berater::Middleware::FailOpen do
|
|
94
94
|
end
|
95
95
|
end
|
96
96
|
end
|
97
|
+
|
98
|
+
context 'when there is no lock' do
|
99
|
+
it 'does not crash' do
|
100
|
+
instance.call {}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'when the lock is not a lock' do
|
105
|
+
it 'does not crash' do
|
106
|
+
instance.call { :foo }
|
107
|
+
end
|
108
|
+
end
|
97
109
|
end
|
98
110
|
|
99
111
|
context 'when there is an error during lock acquisition' do
|
@@ -2,8 +2,6 @@ describe Berater::Middleware::LoadShedder do
|
|
2
2
|
describe '#call' do
|
3
3
|
subject { described_class.new }
|
4
4
|
|
5
|
-
before { Berater.test_mode = :pass }
|
6
|
-
|
7
5
|
it 'yields' do
|
8
6
|
expect {|b| subject.call(&b) }.to yield_control
|
9
7
|
end
|
@@ -46,20 +44,6 @@ describe Berater::Middleware::LoadShedder do
|
|
46
44
|
end
|
47
45
|
end
|
48
46
|
|
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
47
|
it 'works with a fractional priority' do
|
64
48
|
subject.call(capacity: 100, priority: 1.5) do |capacity:|
|
65
49
|
expect(capacity).to be < 100
|
@@ -81,6 +65,42 @@ describe Berater::Middleware::LoadShedder do
|
|
81
65
|
end
|
82
66
|
end
|
83
67
|
end
|
68
|
+
|
69
|
+
context 'with a stringified priority' do
|
70
|
+
it 'casts the value' do
|
71
|
+
subject.call(capacity: 100, priority: '5') do |capacity:|
|
72
|
+
expect(capacity).to eq 60
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'with a bogus priority value' do
|
78
|
+
it 'ignores the priority option' do
|
79
|
+
subject.call(capacity: 100, priority: nil) do |capacity:|
|
80
|
+
expect(capacity).to eq 100
|
81
|
+
end
|
82
|
+
|
83
|
+
subject.call(capacity: 100, priority: -1) do |capacity:|
|
84
|
+
expect(capacity).to eq 100
|
85
|
+
end
|
86
|
+
|
87
|
+
subject.call(capacity: 100, priority: 0) do |capacity:|
|
88
|
+
expect(capacity).to eq 100
|
89
|
+
end
|
90
|
+
|
91
|
+
subject.call(capacity: 100, priority: 50) do |capacity:|
|
92
|
+
expect(capacity).to eq 100
|
93
|
+
end
|
94
|
+
|
95
|
+
subject.call(capacity: 100, priority: 'abc') do |capacity:|
|
96
|
+
expect(capacity).to eq 100
|
97
|
+
end
|
98
|
+
|
99
|
+
subject.call(capacity: 100, priority: :abc) do |capacity:|
|
100
|
+
expect(capacity).to eq 100
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
84
104
|
end
|
85
105
|
|
86
106
|
context 'with a limiter' do
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'datadog/statsd'
|
2
|
+
|
3
|
+
describe Berater::Middleware::Statsd do
|
4
|
+
let(:client) { double(Datadog::Statsd) }
|
5
|
+
|
6
|
+
before do
|
7
|
+
allow(client).to receive(:gauge)
|
8
|
+
allow(client).to receive(:increment)
|
9
|
+
allow(client).to receive(:timing)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#call' do
|
13
|
+
subject { described_class.new(client, **client_opts).call(limiter, **opts, &block) }
|
14
|
+
|
15
|
+
let(:client_opts) { {} }
|
16
|
+
let(:limiter) { double(Berater::Limiter, key: :key, capacity: 5) }
|
17
|
+
let(:lock) { double(Berater::Lock, capacity: 4, contention: 2) }
|
18
|
+
let(:opts) { { capacity: lock.capacity, cost: 1 } }
|
19
|
+
let(:block) { lambda { lock } }
|
20
|
+
|
21
|
+
after { subject }
|
22
|
+
|
23
|
+
it 'returns a lock' do
|
24
|
+
expect(subject).to be lock
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'tracks the call' do
|
28
|
+
expect(client).to receive(:timing).with(
|
29
|
+
'berater.limiter.limit',
|
30
|
+
Float,
|
31
|
+
tags: {
|
32
|
+
key: limiter.key,
|
33
|
+
limiter: String,
|
34
|
+
overloaded: false,
|
35
|
+
},
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'tracks limiter capacity' do
|
40
|
+
expect(client).to receive(:gauge).with(
|
41
|
+
'berater.limiter.capacity',
|
42
|
+
limiter.capacity,
|
43
|
+
Hash
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when custom tags are passed in' do
|
48
|
+
let(:opts) { { tags: { abc: 123 } } }
|
49
|
+
|
50
|
+
it 'incorporates the tags' do
|
51
|
+
expect(client).to receive(:timing) do |*, tags:|
|
52
|
+
expect(tags).to include(opts[:tags])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'with global tags' do
|
58
|
+
let(:client_opts) { { tags: { abc: 123 } }}
|
59
|
+
|
60
|
+
it 'incorporates the tags' do
|
61
|
+
expect(client).to receive(:timing) do |*, tags:|
|
62
|
+
expect(tags).to include(client_opts[:tags])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'with global tag callback' do
|
68
|
+
let(:client_opts) { { tags: callback }}
|
69
|
+
let(:callback) { double(Proc) }
|
70
|
+
|
71
|
+
it 'calls the callback' do
|
72
|
+
expect(callback).to receive(:call).with(limiter, **opts)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'incorporates the tags' do
|
76
|
+
expect(callback).to receive(:call).and_return({ abc: 123 })
|
77
|
+
|
78
|
+
expect(client).to receive(:timing) do |*, tags:|
|
79
|
+
expect(tags).to include(abc: 123)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'with a limiter' do
|
86
|
+
before do
|
87
|
+
Berater.middleware.use described_class, client
|
88
|
+
end
|
89
|
+
|
90
|
+
let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 3) }
|
91
|
+
|
92
|
+
it 'tracks calls to limit' do
|
93
|
+
expect(client).to receive(:timing) do |*, tags:|
|
94
|
+
expect(tags[:limiter]).to eq 'ConcurrencyLimiter'
|
95
|
+
end
|
96
|
+
|
97
|
+
expect(client).to receive(:gauge).with(
|
98
|
+
'berater.limiter.capacity',
|
99
|
+
limiter.capacity,
|
100
|
+
Hash,
|
101
|
+
)
|
102
|
+
|
103
|
+
expect(client).to receive(:gauge).with(
|
104
|
+
'berater.limiter.contention',
|
105
|
+
1,
|
106
|
+
Hash,
|
107
|
+
)
|
108
|
+
|
109
|
+
limiter.limit
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'tracks each call' do
|
113
|
+
expect(client).to receive(:gauge).with(
|
114
|
+
'berater.limiter.contention',
|
115
|
+
1,
|
116
|
+
Hash,
|
117
|
+
)
|
118
|
+
|
119
|
+
expect(client).to receive(:gauge).with(
|
120
|
+
'berater.limiter.contention',
|
121
|
+
2,
|
122
|
+
Hash,
|
123
|
+
)
|
124
|
+
|
125
|
+
2.times { limiter.limit }
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'when an exception is raised' do
|
129
|
+
before do
|
130
|
+
expect(limiter).to receive(:redis).and_raise(error)
|
131
|
+
end
|
132
|
+
|
133
|
+
let(:error) { Redis::TimeoutError }
|
134
|
+
|
135
|
+
it 'tracks limiter exceptions' do
|
136
|
+
expect(client).to receive(:increment).with(
|
137
|
+
'berater.limiter.error',
|
138
|
+
tags: hash_including(type: error.to_s),
|
139
|
+
)
|
140
|
+
|
141
|
+
expect { limiter.limit }.to raise_error(error)
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'with FailOpen middleware inserted after' do
|
145
|
+
before do
|
146
|
+
Berater.middleware.use Berater::Middleware::FailOpen
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'does not track the exception' do
|
150
|
+
expect(client).not_to receive(:increment).with(
|
151
|
+
'berater.limiter.error',
|
152
|
+
)
|
153
|
+
|
154
|
+
limiter.limit
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'does not track lock-based stats' do
|
158
|
+
expect(client).not_to receive(:gauge).with(
|
159
|
+
'berater.lock.capacity',
|
160
|
+
)
|
161
|
+
|
162
|
+
expect(client).not_to receive(:gauge).with(
|
163
|
+
'berater.limiter.contention',
|
164
|
+
)
|
165
|
+
|
166
|
+
limiter.limit
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context 'with FailOpen middleware inserted before' do
|
171
|
+
before do
|
172
|
+
Berater.middleware.prepend Berater::Middleware::FailOpen
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'tracks the exception' do
|
176
|
+
expect(client).to receive(:increment).with(
|
177
|
+
'berater.limiter.error',
|
178
|
+
Hash,
|
179
|
+
)
|
180
|
+
|
181
|
+
limiter.limit
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'does not track lock-based stats' do
|
185
|
+
expect(client).not_to receive(:gauge).with(
|
186
|
+
'berater.lock.capacity',
|
187
|
+
)
|
188
|
+
|
189
|
+
expect(client).not_to receive(:gauge).with(
|
190
|
+
'berater.limiter.contention',
|
191
|
+
)
|
192
|
+
|
193
|
+
limiter.limit
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context 'when the limiter is overloaded' do
|
199
|
+
before { limiter.capacity.times { limiter.limit } }
|
200
|
+
|
201
|
+
after do
|
202
|
+
expect { limiter.limit }.to be_overloaded
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'tracks the limit call' do
|
206
|
+
expect(client).to receive(:timing).with(
|
207
|
+
'berater.limiter.limit',
|
208
|
+
Float,
|
209
|
+
tags: hash_including(overloaded: true),
|
210
|
+
)
|
211
|
+
end
|
212
|
+
|
213
|
+
it 'tracks contention' do
|
214
|
+
expect(client).not_to receive(:gauge).with(
|
215
|
+
'berater.limiter.contention',
|
216
|
+
limiter.capacity,
|
217
|
+
)
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'does not track the exception' do
|
221
|
+
expect(client).not_to receive(:increment).with(
|
222
|
+
'berater.limiter.error',
|
223
|
+
)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
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.12.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-11-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: meddleware
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: dogstatsd-ruby
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '4.3'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '4.3'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: rake
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -153,6 +167,7 @@ files:
|
|
153
167
|
- lib/berater/middleware.rb
|
154
168
|
- lib/berater/middleware/fail_open.rb
|
155
169
|
- lib/berater/middleware/load_shedder.rb
|
170
|
+
- lib/berater/middleware/statsd.rb
|
156
171
|
- lib/berater/rate_limiter.rb
|
157
172
|
- lib/berater/rspec.rb
|
158
173
|
- lib/berater/rspec/matchers.rb
|
@@ -172,6 +187,7 @@ files:
|
|
172
187
|
- spec/matchers_spec.rb
|
173
188
|
- spec/middleware/fail_open_spec.rb
|
174
189
|
- spec/middleware/load_shedder_spec.rb
|
190
|
+
- spec/middleware/statsd_spec.rb
|
175
191
|
- spec/middleware_spec.rb
|
176
192
|
- spec/rate_limiter_spec.rb
|
177
193
|
- spec/riddle_spec.rb
|
@@ -205,6 +221,7 @@ summary: Berater
|
|
205
221
|
test_files:
|
206
222
|
- spec/rate_limiter_spec.rb
|
207
223
|
- spec/middleware/load_shedder_spec.rb
|
224
|
+
- spec/middleware/statsd_spec.rb
|
208
225
|
- spec/middleware/fail_open_spec.rb
|
209
226
|
- spec/matchers_spec.rb
|
210
227
|
- spec/dsl_refinement_spec.rb
|