berater 0.11.1 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8b657c7eb3a69e868416ad2d149d6c35ab13989e77a16ece23217deaaa9fafb6
4
- data.tar.gz: 30835e7c247da37543ea118067d0b61d16080399ff6b53e86783c53a94b8d560
3
+ metadata.gz: 72cc5fcf9c68fff6dd3988ff85897b38d6fdcff21c0e6f94d3ec098ca8b35fc7
4
+ data.tar.gz: 49144f0dee2d7197740d39b9da365f9d959c730b7ece1ae761948ec9263ada75
5
5
  SHA512:
6
- metadata.gz: '04481aa9c3097930f66eb29c6e19e650309b882cf835c1f36cb3fa58524b957d1194b5acc6a83f9fdfd33b73f888261ff3cc3171d730fd9781d9cf768b2b0768'
7
- data.tar.gz: e0cd1a220a418face72685b44d23dff0040dc5f3d94b8c0d82769123a1c387e709702e8f093cc9c0f600d002ea4c848ebd4f0a2e38a4368e743104fbee597f9d
6
+ metadata.gz: 401a193c358a80098f1250053983d8f1a362cd14649156a036deedeb461fd70202743764d375b04a2b6b4fb08c795ec61d013e1be342626a2c8aad7dab2cb259
7
+ data.tar.gz: 55498f8b1a6bd70a64ed9053b80bc9c95b0d60043f073c302d48bb79b51677d7a3c235c07f35cc51dacec067d0c2cc8dcf972e37fb9e9baae126f69fa8aacb4f
@@ -14,6 +14,8 @@ module Berater
14
14
 
15
15
  def call(*, **opts)
16
16
  yield.tap do |lock|
17
+ return lock unless lock&.is_a?(Berater::Lock)
18
+
17
19
  # wrap lock.release so it fails open
18
20
 
19
21
  # save reference to original function
@@ -18,6 +18,11 @@ module Berater
18
18
  protected
19
19
 
20
20
  def adjust_capacity(capacity, priority)
21
+ if priority.is_a?(String)
22
+ # try casting
23
+ priority = Float(priority) rescue nil
24
+ end
25
+
21
26
  unless PRIORITY_RANGE.include?(priority)
22
27
  return capacity
23
28
  end
@@ -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
@@ -2,5 +2,6 @@ module Berater
2
2
  module Middleware
3
3
  autoload 'FailOpen', 'berater/middleware/fail_open'
4
4
  autoload 'LoadShedder', 'berater/middleware/load_shedder'
5
+ autoload 'Statsd', 'berater/middleware/statsd'
5
6
  end
6
7
  end
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = "0.11.1"
2
+ VERSION = "0.12.0"
3
3
  end
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 { Berater::Unlimiter.new(:key, capacity).capacity }
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 gracefully' do
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.11.1
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-10-25 00:00:00.000000000 Z
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