berater 0.11.1 → 0.12.2

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