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 +4 -4
- data/lib/berater/concurrency_limiter.rb +1 -1
- data/lib/berater/limiter.rb +7 -7
- data/lib/berater/middleware/fail_open.rb +2 -0
- data/lib/berater/middleware/load_shedder.rb +5 -0
- data/lib/berater/middleware/statsd.rb +92 -0
- data/lib/berater/middleware/trace.rb +41 -0
- data/lib/berater/middleware.rb +2 -0
- data/lib/berater/rate_limiter.rb +1 -2
- data/lib/berater/static_limiter.rb +1 -1
- data/lib/berater/test_mode.rb +3 -1
- data/lib/berater/version.rb +1 -1
- data/lib/berater.rb +2 -2
- data/spec/berater_spec.rb +7 -4
- data/spec/limiter_spec.rb +36 -7
- data/spec/middleware/fail_open_spec.rb +14 -0
- data/spec/middleware/load_shedder_spec.rb +38 -16
- data/spec/middleware/statsd_spec.rb +261 -0
- data/spec/middleware/trace_spec.rb +58 -0
- data/spec/test_mode_spec.rb +32 -6
- metadata +36 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 250bedb127940f561746dc7385129f30a2a17b677501bb0f864596f8c3de30f9
|
4
|
+
data.tar.gz: 6adb5fa4d20cd133a4eb87e1406977619d9b5034f3b8ed8a576dcee0c36333cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72b457fc673453f21b75635776b37857f5f6f79f9bb90ba17b42b8e51f3351b427770ddeead2012bab18b32024ca3c850112868c824edb521b6e82bb7bcfc681
|
7
|
+
data.tar.gz: a5e81cda6ff755e6a19b12e18ca2a99b457879e7047845604bf533505bb4f3c95e6ac5bf032fb119a0115dcacc246dd616d94007a16d0fcd471f4851ea69f497
|
data/lib/berater/limiter.rb
CHANGED
@@ -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] ||=
|
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
|
114
|
+
def acquire_lock(capacity:, cost:)
|
114
115
|
raise NotImplementedError
|
115
116
|
end
|
116
117
|
|
117
|
-
def cache_key
|
118
|
-
|
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
|
@@ -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
|
data/lib/berater/middleware.rb
CHANGED
data/lib/berater/rate_limiter.rb
CHANGED
@@ -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
|
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
|
|
data/lib/berater/test_mode.rb
CHANGED
@@ -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
|
data/lib/berater/version.rb
CHANGED
data/lib/berater.rb
CHANGED
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)
|
84
|
+
expect(limiter).to receive(:limit)
|
85
|
+
|
87
86
|
subject
|
88
87
|
end
|
89
88
|
|
90
89
|
it 'yields' do
|
91
|
-
|
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
|
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
|
@@ -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(
|
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(
|
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
|
68
|
-
|
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(
|
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
|
data/spec/test_mode_spec.rb
CHANGED
@@ -9,8 +9,13 @@ describe Berater::TestMode do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
it 'prepends Limiter subclasses' do
|
12
|
-
expect(Berater::
|
13
|
-
expect(Berater::
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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:
|
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
|