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 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