berater 0.11.1 → 0.12.2

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: 250bedb127940f561746dc7385129f30a2a17b677501bb0f864596f8c3de30f9
4
+ data.tar.gz: 6adb5fa4d20cd133a4eb87e1406977619d9b5034f3b8ed8a576dcee0c36333cb
5
5
  SHA512:
6
- metadata.gz: '04481aa9c3097930f66eb29c6e19e650309b882cf835c1f36cb3fa58524b957d1194b5acc6a83f9fdfd33b73f888261ff3cc3171d730fd9781d9cf768b2b0768'
7
- data.tar.gz: e0cd1a220a418face72685b44d23dff0040dc5f3d94b8c0d82769123a1c387e709702e8f093cc9c0f600d002ea4c848ebd4f0a2e38a4368e743104fbee597f9d
6
+ metadata.gz: 72b457fc673453f21b75635776b37857f5f6f79f9bb90ba17b42b8e51f3351b427770ddeead2012bab18b32024ca3c850112868c824edb521b6e82bb7bcfc681
7
+ data.tar.gz: a5e81cda6ff755e6a19b12e18ca2a99b457879e7047845604bf533505bb4f3c95e6ac5bf032fb119a0115dcacc246dd616d94007a16d0fcd471f4851ea69f497
@@ -63,7 +63,7 @@ module Berater
63
63
  LUA
64
64
  )
65
65
 
66
- protected def acquire_lock(capacity, cost)
66
+ protected def acquire_lock(capacity:, cost:)
67
67
  # round fractional capacity and cost
68
68
  capacity = capacity.to_i
69
69
  cost = cost.ceil
@@ -1,5 +1,6 @@
1
1
  module Berater
2
2
  class Limiter
3
+ DEFAULT_COST = 1
3
4
 
4
5
  attr_reader :key, :capacity, :options
5
6
 
@@ -9,7 +10,7 @@ module Berater
9
10
 
10
11
  def limit(**opts, &block)
11
12
  opts[:capacity] ||= @capacity
12
- opts[:cost] ||= 1
13
+ opts[:cost] ||= DEFAULT_COST
13
14
 
14
15
  lock = Berater.middleware.call(self, **opts) do |limiter, **opts|
15
16
  limiter.inner_limit(**opts)
@@ -26,7 +27,7 @@ module Berater
26
27
  end
27
28
  end
28
29
 
29
- protected def inner_limit(capacity:, cost:)
30
+ protected def inner_limit(capacity:, cost:, **opts)
30
31
  if capacity.is_a?(String)
31
32
  # try casting
32
33
  begin
@@ -49,7 +50,7 @@ module Berater
49
50
  raise ArgumentError, "invalid cost: #{cost}"
50
51
  end
51
52
 
52
- acquire_lock(capacity, cost)
53
+ acquire_lock(capacity: capacity, cost: cost, **opts)
53
54
  rescue NoMethodError => e
54
55
  raise unless e.message.include?("undefined method `evalsha' for")
55
56
 
@@ -110,13 +111,12 @@ module Berater
110
111
  @capacity = capacity
111
112
  end
112
113
 
113
- def acquire_lock(capacity, cost)
114
+ def acquire_lock(capacity:, cost:)
114
115
  raise NotImplementedError
115
116
  end
116
117
 
117
- def cache_key(subkey = nil)
118
- instance_key = subkey.nil? ? key : "#{key}:#{subkey}"
119
- self.class.cache_key(instance_key)
118
+ def cache_key
119
+ self.class.cache_key(key)
120
120
  end
121
121
 
122
122
  class << self
@@ -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,92 @@
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
+ # capture exception for reporting, then 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,
25
+ )
26
+
27
+ @client.gauge(
28
+ 'berater.limiter.capacity',
29
+ limiter.capacity,
30
+ tags: tags,
31
+ )
32
+
33
+ if lock
34
+ @client.increment(
35
+ 'berater.lock.acquired',
36
+ tags: tags,
37
+ )
38
+
39
+ if lock.contention >= 0 # not a failsafe lock
40
+ @client.gauge(
41
+ 'berater.lock.capacity',
42
+ lock.capacity,
43
+ tags: tags,
44
+ )
45
+ @client.gauge(
46
+ 'berater.lock.contention',
47
+ lock.contention,
48
+ tags: tags,
49
+ )
50
+ end
51
+ end
52
+
53
+ if error
54
+ if error.is_a?(Berater::Overloaded)
55
+ @client.increment(
56
+ 'berater.limiter.overloaded',
57
+ tags: tags,
58
+ )
59
+ else
60
+ @client.increment(
61
+ 'berater.limiter.error',
62
+ tags: tags.merge(type: error.class.to_s.gsub('::', '_'))
63
+ )
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def build_tags(limiter, opts)
71
+ tags = {
72
+ key: limiter.key,
73
+ limiter: limiter.class.to_s.split(':')[-1],
74
+ }
75
+
76
+ # append global custom tags
77
+ if @tags
78
+ if @tags.respond_to?(:call)
79
+ tags.merge!(@tags.call(limiter, **opts) || {})
80
+ else
81
+ tags.merge!(@tags)
82
+ end
83
+ end
84
+
85
+ # append call specific custom tags
86
+ tags.merge!(opts[:tags]) if opts[:tags]
87
+
88
+ tags
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,41 @@
1
+ require 'ddtrace'
2
+
3
+ module Berater
4
+ module Middleware
5
+ class Trace
6
+ def initialize(tracer: nil)
7
+ @tracer = tracer
8
+ end
9
+
10
+ def call(limiter, **)
11
+ tracer.trace('Berater.limit') do |span|
12
+ begin
13
+ lock = yield
14
+ rescue Exception => error
15
+ # capture exception for reporting, then propagate
16
+ raise
17
+ ensure
18
+ span.set_tag('capacity', limiter.capacity)
19
+ span.set_tag('contention', lock.contention) if lock
20
+ span.set_tag('key', limiter.key)
21
+ span.set_tag('limiter', limiter.class.to_s.split(':')[-1])
22
+
23
+ if error
24
+ if error.is_a?(Berater::Overloaded)
25
+ span.set_tag('overloaded', true)
26
+ else
27
+ span.set_tag('error', error.class.to_s.gsub('::', '_'))
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def tracer
37
+ @tracer || Datadog.tracer
38
+ end
39
+ end
40
+ end
41
+ end
@@ -2,5 +2,7 @@ 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'
6
+ autoload 'Trace', 'berater/middleware/trace'
5
7
  end
6
8
  end
@@ -20,7 +20,6 @@ module Berater
20
20
 
21
21
  LUA_SCRIPT = Berater::LuaScript(<<~LUA
22
22
  local key = KEYS[1]
23
- local ts_key = KEYS[2]
24
23
  local ts = tonumber(ARGV[1])
25
24
  local capacity = tonumber(ARGV[2])
26
25
  local interval_msec = tonumber(ARGV[3])
@@ -71,7 +70,7 @@ module Berater
71
70
  LUA
72
71
  )
73
72
 
74
- protected def acquire_lock(capacity, cost)
73
+ protected def acquire_lock(capacity:, cost:)
75
74
  # timestamp in milliseconds
76
75
  ts = (Time.now.to_f * 10**3).to_i
77
76
 
@@ -18,7 +18,7 @@ module Berater
18
18
  LUA
19
19
  )
20
20
 
21
- protected def acquire_lock(capacity, cost)
21
+ protected def acquire_lock(capacity:, cost:)
22
22
  if cost == 0
23
23
  # utilization check
24
24
  count = redis.get(cache_key) || "0"
@@ -21,7 +21,7 @@ module Berater
21
21
 
22
22
  class Limiter
23
23
  module TestMode
24
- def acquire_lock(*)
24
+ def acquire_lock(**)
25
25
  case Berater.test_mode
26
26
  when :pass
27
27
  Lock.new(Float::INFINITY, 0)
@@ -43,5 +43,7 @@ Berater.singleton_class.prepend Berater::TestMode
43
43
  ObjectSpace.each_object(Class).each do |klass|
44
44
  next unless klass < Berater::Limiter
45
45
 
46
+ next if klass == Berater::Unlimiter || klass == Berater::Inhibitor
47
+
46
48
  klass.prepend Berater::Limiter::TestMode
47
49
  end
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = "0.11.1"
2
+ VERSION = "0.12.2"
3
3
  end
data/lib/berater.rb CHANGED
@@ -52,8 +52,8 @@ module Berater
52
52
  end
53
53
 
54
54
  def expunge
55
- redis.scan_each(match: "#{self.name}*") do |key|
56
- redis.del key
55
+ redis.scan_each(match: "#{name}*") do |key|
56
+ redis.del(key)
57
57
  end
58
58
  end
59
59
 
data/spec/berater_spec.rb CHANGED
@@ -77,18 +77,21 @@ describe Berater do
77
77
  end
78
78
 
79
79
  context 'with a block' do
80
- before { Berater.test_mode = :pass }
81
-
82
80
  subject { Berater(:key, capacity, **opts) { 123 } }
83
81
 
84
82
  it 'creates a limiter and calls limit' do
85
83
  expect(klass).to receive(:new).and_return(limiter)
86
- expect(limiter).to receive(:limit).and_call_original
84
+ expect(limiter).to receive(:limit)
85
+
87
86
  subject
88
87
  end
89
88
 
90
89
  it 'yields' do
91
- is_expected.to be 123
90
+ if capacity > 0
91
+ is_expected.to be 123
92
+ else
93
+ expect { subject }.to be_overloaded
94
+ end
92
95
  end
93
96
  end
94
97
  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
@@ -45,7 +47,9 @@ describe Berater::Limiter do
45
47
 
46
48
  context 'with a capacity parameter' do
47
49
  it 'overrides the stored value' do
48
- is_expected.to receive(:acquire_lock).with(3, anything)
50
+ is_expected.to receive(:acquire_lock).with(hash_including(
51
+ capacity: 3,
52
+ ))
49
53
 
50
54
  subject.limit(capacity: 3)
51
55
  end
@@ -57,15 +61,29 @@ describe Berater::Limiter do
57
61
  end
58
62
 
59
63
  it 'handles stringified numerics gracefully' do
60
- is_expected.to receive(:acquire_lock).with(3.5, anything)
64
+ is_expected.to receive(:acquire_lock).with(hash_including(
65
+ capacity: 3.5,
66
+ ))
61
67
 
62
68
  subject.limit(capacity: '3.5')
63
69
  end
64
70
  end
65
71
 
72
+ it 'has a default cost' do
73
+ is_expected.to receive(:acquire_lock).with(hash_including(
74
+ cost: described_class::DEFAULT_COST,
75
+ ))
76
+
77
+ subject.limit
78
+ end
79
+
66
80
  context 'with a cost parameter' do
67
- it 'overrides the stored value' do
68
- is_expected.to receive(:acquire_lock).with(anything, 2)
81
+ it 'overrides the default value' do
82
+ expect(described_class::DEFAULT_COST).not_to eq 2
83
+
84
+ is_expected.to receive(:acquire_lock).with(hash_including(
85
+ cost: 2,
86
+ ))
69
87
 
70
88
  subject.limit(cost: 2)
71
89
  end
@@ -85,12 +103,23 @@ describe Berater::Limiter do
85
103
  end
86
104
 
87
105
  it 'handles stringified numerics gracefully' do
88
- is_expected.to receive(:acquire_lock).with(anything, 2.5)
106
+ is_expected.to receive(:acquire_lock).with(hash_including(
107
+ cost: 2.5,
108
+ ))
89
109
 
90
110
  subject.limit(cost: '2.5')
91
111
  end
92
112
  end
93
113
 
114
+ it 'passes through arbitrary parameters' do
115
+ is_expected.to receive(:acquire_lock).with(hash_including(
116
+ priority: 123,
117
+ zed: nil,
118
+ ))
119
+
120
+ subject.limit(priority: 123, zed: nil)
121
+ end
122
+
94
123
  context 'when Berater.redis is nil' do
95
124
  let!(:redis) { Berater.redis }
96
125
 
@@ -1,4 +1,6 @@
1
1
  describe Berater::Middleware::FailOpen do
2
+ it_behaves_like 'a limiter middleware'
3
+
2
4
  let(:limiter) { Berater::Unlimiter.new }
3
5
  let(:lock) { limiter.limit }
4
6
  let(:error) { Redis::TimeoutError }
@@ -94,6 +96,18 @@ describe Berater::Middleware::FailOpen do
94
96
  end
95
97
  end
96
98
  end
99
+
100
+ context 'when there is no lock' do
101
+ it 'does not crash' do
102
+ instance.call {}
103
+ end
104
+ end
105
+
106
+ context 'when the lock is not a lock' do
107
+ it 'does not crash' do
108
+ instance.call { :foo }
109
+ end
110
+ end
97
111
  end
98
112
 
99
113
  context 'when there is an error during lock acquisition' do
@@ -1,9 +1,9 @@
1
1
  describe Berater::Middleware::LoadShedder do
2
+ it_behaves_like 'a limiter middleware'
3
+
2
4
  describe '#call' do
3
5
  subject { described_class.new }
4
6
 
5
- before { Berater.test_mode = :pass }
6
-
7
7
  it 'yields' do
8
8
  expect {|b| subject.call(&b) }.to yield_control
9
9
  end
@@ -46,20 +46,6 @@ describe Berater::Middleware::LoadShedder do
46
46
  end
47
47
  end
48
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
49
  it 'works with a fractional priority' do
64
50
  subject.call(capacity: 100, priority: 1.5) do |capacity:|
65
51
  expect(capacity).to be < 100
@@ -81,6 +67,42 @@ describe Berater::Middleware::LoadShedder do
81
67
  end
82
68
  end
83
69
  end
70
+
71
+ context 'with a stringified priority' do
72
+ it 'casts the value' do
73
+ subject.call(capacity: 100, priority: '5') do |capacity:|
74
+ expect(capacity).to eq 60
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'with a bogus priority value' do
80
+ it 'ignores the priority option' do
81
+ subject.call(capacity: 100, priority: nil) do |capacity:|
82
+ expect(capacity).to eq 100
83
+ end
84
+
85
+ subject.call(capacity: 100, priority: -1) do |capacity:|
86
+ expect(capacity).to eq 100
87
+ end
88
+
89
+ subject.call(capacity: 100, priority: 0) do |capacity:|
90
+ expect(capacity).to eq 100
91
+ end
92
+
93
+ subject.call(capacity: 100, priority: 50) do |capacity:|
94
+ expect(capacity).to eq 100
95
+ end
96
+
97
+ subject.call(capacity: 100, priority: 'abc') do |capacity:|
98
+ expect(capacity).to eq 100
99
+ end
100
+
101
+ subject.call(capacity: 100, priority: :abc) do |capacity:|
102
+ expect(capacity).to eq 100
103
+ end
104
+ end
105
+ end
84
106
  end
85
107
 
86
108
  context 'with a limiter' do
@@ -0,0 +1,261 @@
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
+ context do
13
+ before do
14
+ Berater.middleware.use described_class, client
15
+ end
16
+
17
+ it_behaves_like 'a limiter middleware'
18
+ end
19
+
20
+ describe '#call' do
21
+ subject do
22
+ described_class.new(client, **middleware_opts).call(limiter, **opts, &block)
23
+ end
24
+
25
+ let(:middleware_opts) { {} }
26
+ let(:limiter) { double(Berater::Limiter, key: :key, capacity: 5) }
27
+ let(:lock) { double(Berater::Lock, capacity: 4, contention: 2) }
28
+ let(:opts) { { capacity: limiter.capacity, cost: 1 } }
29
+ let(:block) { lambda { lock } }
30
+
31
+ after { subject }
32
+
33
+ it 'returns a lock' do
34
+ expect(subject).to be lock
35
+ end
36
+
37
+ it 'tracks the call' do
38
+ expect(client).to receive(:timing).with(
39
+ 'berater.limiter.limit',
40
+ Float,
41
+ tags: {
42
+ key: limiter.key,
43
+ limiter: String,
44
+ },
45
+ )
46
+ end
47
+
48
+ it 'tracks limiter capacity' do
49
+ expect(client).to receive(:gauge).with(
50
+ 'berater.limiter.capacity',
51
+ limiter.capacity,
52
+ Hash
53
+ )
54
+ end
55
+
56
+ it 'tracks lock acquisition' do
57
+ expect(client).to receive(:increment).with(
58
+ 'berater.lock.acquired',
59
+ Hash
60
+ )
61
+ end
62
+
63
+ it 'tracks lock capacity' do
64
+ expect(client).to receive(:gauge).with(
65
+ 'berater.lock.capacity',
66
+ lock.capacity,
67
+ Hash
68
+ )
69
+ end
70
+
71
+ it 'tracks lock contention' do
72
+ expect(client).to receive(:gauge).with(
73
+ 'berater.lock.contention',
74
+ lock.contention,
75
+ Hash
76
+ )
77
+ end
78
+
79
+ describe 'tags' do
80
+ def expect_tags_to(matcher)
81
+ expect(client).to receive(:timing) do |*, tags:|
82
+ expect(tags).to matcher
83
+ end
84
+ end
85
+
86
+ context 'with global tags' do
87
+ let(:middleware_opts) { { tags: { abc: 123 } }}
88
+
89
+ it 'incorporates the tags' do
90
+ expect_tags_to include(middleware_opts[:tags])
91
+ end
92
+ end
93
+
94
+ context 'with global tag callback' do
95
+ let(:middleware_opts) { { tags: callback }}
96
+ let(:callback) { double(Proc) }
97
+
98
+ it 'calls the callback' do
99
+ expect(callback).to receive(:call).with(limiter, **opts)
100
+ end
101
+
102
+ it 'incorporates the tags' do
103
+ expect(callback).to receive(:call).and_return({ abc: 123 })
104
+ expect_tags_to include(abc: 123)
105
+ end
106
+ end
107
+
108
+ context 'when call specific custom tags are passed in' do
109
+ let(:opts) { { tags: { abc: 123 } } }
110
+
111
+ it 'incorporates the tags' do
112
+ expect_tags_to include(opts[:tags])
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ context 'with a limiter' do
119
+ before do
120
+ Berater.middleware.use described_class, client
121
+ end
122
+
123
+ let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 3) }
124
+
125
+ it 'tracks calls to limit' do
126
+ expect(client).to receive(:timing) do |*, tags:|
127
+ expect(tags[:limiter]).to eq 'ConcurrencyLimiter'
128
+ end
129
+
130
+ expect(client).to receive(:gauge).with(
131
+ 'berater.limiter.capacity',
132
+ limiter.capacity,
133
+ Hash,
134
+ )
135
+
136
+ expect(client).to receive(:gauge).with(
137
+ 'berater.lock.capacity',
138
+ limiter.capacity,
139
+ Hash,
140
+ )
141
+
142
+ expect(client).to receive(:gauge).with(
143
+ 'berater.lock.contention',
144
+ 1,
145
+ Hash,
146
+ )
147
+
148
+ limiter.limit
149
+ end
150
+
151
+ it 'tracks each call' do
152
+ expect(client).to receive(:gauge).with(
153
+ 'berater.lock.contention',
154
+ 1,
155
+ Hash,
156
+ )
157
+
158
+ expect(client).to receive(:gauge).with(
159
+ 'berater.lock.contention',
160
+ 2,
161
+ Hash,
162
+ )
163
+
164
+ 2.times { limiter.limit }
165
+ end
166
+
167
+ context 'when an exception is raised' do
168
+ before do
169
+ expect(limiter).to receive(:redis).and_raise(error)
170
+ end
171
+
172
+ let(:error) { Redis::TimeoutError }
173
+
174
+ it 'tracks limiter exceptions' do
175
+ expect(client).to receive(:increment).with(
176
+ 'berater.limiter.error',
177
+ tags: hash_including(type: 'Redis_TimeoutError'),
178
+ )
179
+
180
+ expect { limiter.limit }.to raise_error(error)
181
+ end
182
+
183
+ context 'with FailOpen middleware inserted after' do
184
+ before do
185
+ Berater.middleware.use Berater::Middleware::FailOpen
186
+ end
187
+
188
+ it 'does not track the exception' do
189
+ expect(client).not_to receive(:increment).with(
190
+ 'berater.limiter.error',
191
+ anything,
192
+ )
193
+
194
+ limiter.limit
195
+ end
196
+
197
+ it 'does not track lock-based stats' do
198
+ expect(client).not_to receive(:gauge).with(
199
+ /berater.lock/,
200
+ any_args,
201
+ )
202
+
203
+ limiter.limit
204
+ end
205
+ end
206
+
207
+ context 'with FailOpen middleware inserted before' do
208
+ before do
209
+ Berater.middleware.prepend Berater::Middleware::FailOpen
210
+ end
211
+
212
+ it 'tracks the exception' do
213
+ expect(client).to receive(:increment).with(
214
+ 'berater.limiter.error',
215
+ anything,
216
+ )
217
+
218
+ limiter.limit
219
+ end
220
+
221
+ it 'does not track lock-based stats' do
222
+ expect(client).not_to receive(:gauge).with(
223
+ /berater.lock/,
224
+ any_args,
225
+ )
226
+
227
+ limiter.limit
228
+ end
229
+ end
230
+ end
231
+
232
+ context 'when the limiter is overloaded' do
233
+ before { limiter.capacity.times { limiter.limit } }
234
+
235
+ after do
236
+ expect { limiter.limit }.to be_overloaded
237
+ end
238
+
239
+ it 'tracks the overloaded count' do
240
+ expect(client).to receive(:increment).with(
241
+ 'berater.limiter.overloaded',
242
+ Hash
243
+ )
244
+ end
245
+
246
+ it 'does not track lock-based stats' do
247
+ expect(client).not_to receive(:gauge).with(
248
+ /berater.lock/,
249
+ any_args,
250
+ )
251
+ end
252
+
253
+ it 'does not track the exception' do
254
+ expect(client).not_to receive(:increment).with(
255
+ 'berater.limiter.error',
256
+ anything,
257
+ )
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,58 @@
1
+ describe Berater::Middleware::Trace do
2
+ before { Datadog.tracer.enabled = false }
3
+
4
+ it_behaves_like 'a limiter middleware'
5
+
6
+ let(:limiter) { Berater::Unlimiter.new }
7
+ let(:span) { double(Datadog::Span, set_tag: nil) }
8
+ let(:tracer) { double(Datadog::Tracer) }
9
+
10
+ before do
11
+ allow(tracer).to receive(:trace) {|&b| b.call(span) }
12
+ end
13
+
14
+ context 'with a provided tracer' do
15
+ let(:instance) { described_class.new(tracer: tracer) }
16
+
17
+ it 'traces' do
18
+ expect(tracer).to receive(:trace).with(/Berater/)
19
+ expect(span).to receive(:set_tag).with(/capacity/, Numeric)
20
+
21
+ instance.call(limiter) {}
22
+ end
23
+
24
+ it 'yields' do
25
+ expect {|b| instance.call(limiter, &b) }.to yield_control
26
+ end
27
+
28
+ context 'when an exception is raised' do
29
+ it 'tags the span and raises' do
30
+ expect(span).to receive(:set_tag).with('error', 'IOError')
31
+
32
+ expect {
33
+ instance.call(limiter) { raise IOError }
34
+ }.to raise_error(IOError)
35
+ end
36
+ end
37
+
38
+ context 'when an Overloaded exception is raised' do
39
+ let(:limiter) { Berater::Inhibitor.new }
40
+
41
+ it 'tags the span as overloaded and raises' do
42
+ expect(span).to receive(:set_tag).with('overloaded', true)
43
+
44
+ expect {
45
+ instance.call(limiter) { raise Berater::Overloaded }
46
+ }.to be_overloaded
47
+ end
48
+ end
49
+ end
50
+
51
+ context 'with the default tracer' do
52
+ it 'uses Datadog.tracer' do
53
+ expect(Datadog).to receive(:tracer).and_return(tracer)
54
+
55
+ described_class.new.call(limiter) {}
56
+ end
57
+ end
58
+ end
@@ -9,8 +9,13 @@ describe Berater::TestMode do
9
9
  end
10
10
 
11
11
  it 'prepends Limiter subclasses' do
12
- expect(Berater::Unlimiter.ancestors).to include(Berater::Limiter::TestMode)
13
- expect(Berater::Inhibitor.ancestors).to include(Berater::Limiter::TestMode)
12
+ expect(Berater::ConcurrencyLimiter.ancestors).to include(Berater::Limiter::TestMode)
13
+ expect(Berater::RateLimiter.ancestors).to include(Berater::Limiter::TestMode)
14
+ end
15
+
16
+ it 'does not modify Unlimiter or Inhibitor' do
17
+ expect(Berater::Unlimiter.ancestors).not_to include(Berater::Limiter::TestMode)
18
+ expect(Berater::Inhibitor.ancestors).not_to include(Berater::Limiter::TestMode)
14
19
  end
15
20
 
16
21
  it 'preserves the original functionality via super' do
@@ -19,6 +24,8 @@ describe Berater::TestMode do
19
24
  end
20
25
 
21
26
  describe '.test_mode' do
27
+ let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
28
+
22
29
  it 'can be turned on' do
23
30
  Berater.test_mode = :pass
24
31
  expect(Berater.test_mode).to be :pass
@@ -37,7 +44,6 @@ describe Berater::TestMode do
37
44
  end
38
45
 
39
46
  it 'works no matter when limiter was created' do
40
- limiter = Berater::Unlimiter.new
41
47
  expect(limiter).not_to be_overloaded
42
48
 
43
49
  Berater.test_mode = :fail
@@ -47,7 +53,7 @@ describe Berater::TestMode do
47
53
  it 'supports a generic expectation' do
48
54
  Berater.test_mode = :pass
49
55
  expect_any_instance_of(Berater::Limiter).to receive(:limit)
50
- Berater::Unlimiter.new.limit
56
+ limiter.limit
51
57
  end
52
58
  end
53
59
 
@@ -94,7 +100,17 @@ describe Berater::TestMode do
94
100
  it_behaves_like 'it is not overloaded'
95
101
  end
96
102
 
97
- it_behaves_like 'it supports test_mode'
103
+ context 'when test_mode = :pass' do
104
+ before { Berater.test_mode = :pass }
105
+
106
+ it_behaves_like 'it is not overloaded'
107
+ end
108
+
109
+ context 'when test_mode = :fail' do
110
+ before { Berater.test_mode = :fail }
111
+
112
+ it_behaves_like 'it is not overloaded'
113
+ end
98
114
  end
99
115
 
100
116
  describe 'Inhibitor' do
@@ -106,7 +122,17 @@ describe Berater::TestMode do
106
122
  it_behaves_like 'it is overloaded'
107
123
  end
108
124
 
109
- it_behaves_like 'it supports test_mode'
125
+ context 'when test_mode = :pass' do
126
+ before { Berater.test_mode = :pass }
127
+
128
+ it_behaves_like 'it is overloaded'
129
+ end
130
+
131
+ context 'when test_mode = :fail' do
132
+ before { Berater.test_mode = :fail }
133
+
134
+ it_behaves_like 'it is overloaded'
135
+ end
110
136
  end
111
137
 
112
138
  describe 'RateLimiter' do
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.2
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: 2022-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: meddleware
@@ -80,6 +80,34 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ddtrace
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: dogstatsd-ruby
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '4.3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '4.3'
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: rake
85
113
  requirement: !ruby/object:Gem::Requirement
@@ -153,6 +181,8 @@ files:
153
181
  - lib/berater/middleware.rb
154
182
  - lib/berater/middleware/fail_open.rb
155
183
  - lib/berater/middleware/load_shedder.rb
184
+ - lib/berater/middleware/statsd.rb
185
+ - lib/berater/middleware/trace.rb
156
186
  - lib/berater/rate_limiter.rb
157
187
  - lib/berater/rspec.rb
158
188
  - lib/berater/rspec/matchers.rb
@@ -172,6 +202,8 @@ files:
172
202
  - spec/matchers_spec.rb
173
203
  - spec/middleware/fail_open_spec.rb
174
204
  - spec/middleware/load_shedder_spec.rb
205
+ - spec/middleware/statsd_spec.rb
206
+ - spec/middleware/trace_spec.rb
175
207
  - spec/middleware_spec.rb
176
208
  - spec/rate_limiter_spec.rb
177
209
  - spec/riddle_spec.rb
@@ -205,6 +237,8 @@ summary: Berater
205
237
  test_files:
206
238
  - spec/rate_limiter_spec.rb
207
239
  - spec/middleware/load_shedder_spec.rb
240
+ - spec/middleware/statsd_spec.rb
241
+ - spec/middleware/trace_spec.rb
208
242
  - spec/middleware/fail_open_spec.rb
209
243
  - spec/matchers_spec.rb
210
244
  - spec/dsl_refinement_spec.rb