berater 0.8.0 → 0.11.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 +4 -4
- data/lib/berater/limiter.rb +48 -26
- data/lib/berater/limiter_set.rb +66 -0
- data/lib/berater/lock.rb +1 -1
- data/lib/berater/lua_script.rb +4 -3
- data/lib/berater/middleware/fail_open.rb +41 -0
- data/lib/berater/middleware/load_shedder.rb +31 -0
- data/lib/berater/middleware.rb +6 -0
- data/lib/berater/utils.rb +1 -6
- data/lib/berater/version.rb +1 -1
- data/lib/berater.rb +16 -2
- data/spec/berater_spec.rb +20 -2
- data/spec/limiter_set_spec.rb +173 -0
- data/spec/limiter_spec.rb +41 -2
- data/spec/lua_script_spec.rb +0 -1
- data/spec/middleware/fail_open_spec.rb +184 -0
- data/spec/middleware/load_shedder_spec.rb +130 -0
- data/spec/middleware_spec.rb +120 -0
- metadata +31 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f57ab651eeb34e6a0bb7b889fefa9c13d1d1fc5b6e834bc2b9a6e0ba7b74a8e2
|
4
|
+
data.tar.gz: 8112aa91ae48872132d8222a07f03dce42ff6f5ff26a3bf17906a389ba675ec9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17ae5da54dcf9535d4afe93d7e239ddc1fefaedf3419883cfa22d1f55c0c0aa7a9365986509e8e54e37c08c258068db4a76064dcc6af3f654e170b3efc8c1cd0
|
7
|
+
data.tar.gz: 04b1e7553e86107bbc91fc486b7b0ae099f5575b8654f7800f2a1c281b7b8bfde1e63df2378f03fcb4a9864b15b6a5de610bb6e7a1ce4d4b8e53898d9f4f0869
|
data/lib/berater/limiter.rb
CHANGED
@@ -7,19 +7,14 @@ module Berater
|
|
7
7
|
options[:redis] || Berater.redis
|
8
8
|
end
|
9
9
|
|
10
|
-
def limit(
|
11
|
-
capacity ||= @capacity
|
10
|
+
def limit(**opts, &block)
|
11
|
+
opts[:capacity] ||= @capacity
|
12
|
+
opts[:cost] ||= 1
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
unless cost.is_a?(Numeric) && cost >= 0 && cost < Float::INFINITY
|
18
|
-
raise ArgumentError, "invalid cost: #{cost}"
|
14
|
+
lock = Berater.middleware.call(self, **opts) do |limiter, **opts|
|
15
|
+
limiter.inner_limit(**opts)
|
19
16
|
end
|
20
17
|
|
21
|
-
lock = acquire_lock(capacity, cost)
|
22
|
-
|
23
18
|
if block_given?
|
24
19
|
begin
|
25
20
|
yield lock
|
@@ -31,6 +26,23 @@ module Berater
|
|
31
26
|
end
|
32
27
|
end
|
33
28
|
|
29
|
+
protected def inner_limit(capacity:, cost:)
|
30
|
+
unless capacity.is_a?(Numeric) && capacity >= 0
|
31
|
+
raise ArgumentError, "invalid capacity: #{capacity}"
|
32
|
+
end
|
33
|
+
|
34
|
+
unless cost.is_a?(Numeric) && cost >= 0 && cost < Float::INFINITY
|
35
|
+
raise ArgumentError, "invalid cost: #{cost}"
|
36
|
+
end
|
37
|
+
|
38
|
+
acquire_lock(capacity, cost)
|
39
|
+
rescue NoMethodError => e
|
40
|
+
raise unless e.message.include?("undefined method `evalsha' for")
|
41
|
+
|
42
|
+
# repackage error so it's easier to understand
|
43
|
+
raise RuntimeError, "invalid redis connection: #{redis}"
|
44
|
+
end
|
45
|
+
|
34
46
|
def utilization
|
35
47
|
lock = limit(cost: 0)
|
36
48
|
|
@@ -52,13 +64,6 @@ module Berater
|
|
52
64
|
self.redis.connection == other.redis.connection
|
53
65
|
end
|
54
66
|
|
55
|
-
def self.new(*)
|
56
|
-
# can only call via subclass
|
57
|
-
raise NoMethodError if self == Berater::Limiter
|
58
|
-
|
59
|
-
super
|
60
|
-
end
|
61
|
-
|
62
67
|
protected
|
63
68
|
|
64
69
|
attr_reader :args
|
@@ -93,16 +98,33 @@ module Berater
|
|
93
98
|
self.class.cache_key(instance_key)
|
94
99
|
end
|
95
100
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
101
|
+
class << self
|
102
|
+
def new(*args, **kwargs)
|
103
|
+
# can only call via subclass
|
104
|
+
raise NoMethodError if self == Berater::Limiter
|
100
105
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
+
if RUBY_VERSION < '3' && kwargs.empty?
|
107
|
+
# avoid ruby 2 problems with empty hashes
|
108
|
+
super(*args)
|
109
|
+
else
|
110
|
+
super
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def cache_key(key)
|
115
|
+
klass = to_s.split(':')[-1]
|
116
|
+
"Berater:#{klass}:#{key}"
|
117
|
+
end
|
118
|
+
|
119
|
+
protected
|
120
|
+
|
121
|
+
def inherited(subclass)
|
122
|
+
# automagically create convenience method
|
123
|
+
name = subclass.to_s.split(':')[-1]
|
124
|
+
|
125
|
+
Berater.define_singleton_method(name) do |*args, **opts, &block|
|
126
|
+
Berater::Utils.convenience_fn(subclass, *args, **opts, &block)
|
127
|
+
end
|
106
128
|
end
|
107
129
|
end
|
108
130
|
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Berater
|
2
|
+
private
|
3
|
+
|
4
|
+
class LimiterSet
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@limiters = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
@limiters.each_value(&block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def <<(limiter)
|
16
|
+
key = limiter.key if limiter.respond_to?(:key)
|
17
|
+
send(:[]=, key, limiter)
|
18
|
+
end
|
19
|
+
|
20
|
+
def []=(key, limiter)
|
21
|
+
unless limiter.is_a? Berater::Limiter
|
22
|
+
raise ArgumentError, "expected Berater::Limiter, found: #{limiter}"
|
23
|
+
end
|
24
|
+
|
25
|
+
@limiters[key] = limiter
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](key)
|
29
|
+
@limiters[key]
|
30
|
+
end
|
31
|
+
|
32
|
+
def fetch(key, val = default = true, &block)
|
33
|
+
args = default ? [ key ] : [ key, val ]
|
34
|
+
@limiters.fetch(*args, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def include?(key)
|
38
|
+
if key.is_a? Berater::Limiter
|
39
|
+
@limiters.value?(key)
|
40
|
+
else
|
41
|
+
@limiters.key?(key)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def clear
|
46
|
+
@limiters.clear
|
47
|
+
end
|
48
|
+
|
49
|
+
def count
|
50
|
+
@limiters.count
|
51
|
+
end
|
52
|
+
|
53
|
+
def delete(key)
|
54
|
+
if key.is_a? Berater::Limiter
|
55
|
+
@limiters.delete(key.key)
|
56
|
+
else
|
57
|
+
@limiters.delete(key)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
alias remove delete
|
61
|
+
|
62
|
+
def empty?
|
63
|
+
@limiters.empty?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/berater/lock.rb
CHANGED
data/lib/berater/lua_script.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'digest'
|
2
|
+
require 'redis'
|
2
3
|
|
3
4
|
module Berater
|
4
5
|
class LuaScript
|
@@ -6,11 +7,11 @@ module Berater
|
|
6
7
|
attr_reader :source
|
7
8
|
|
8
9
|
def initialize(source)
|
9
|
-
@source = source
|
10
|
+
@source = source.dup.freeze
|
10
11
|
end
|
11
12
|
|
12
13
|
def sha
|
13
|
-
@sha ||= Digest::SHA1.hexdigest(minify)
|
14
|
+
@sha ||= Digest::SHA1.hexdigest(minify).freeze
|
14
15
|
end
|
15
16
|
|
16
17
|
def eval(redis, *args)
|
@@ -44,7 +45,7 @@ module Berater
|
|
44
45
|
def minify
|
45
46
|
# trim comments (whole line and partial)
|
46
47
|
# and whitespace (prefix and empty lines)
|
47
|
-
@minify ||= source.gsub(/^\s*--.*\n|\s*--.*|^\s*|^$\n/, '').chomp
|
48
|
+
@minify ||= source.gsub(/^\s*--.*\n|\s*--.*|^\s*|^$\n/, '').chomp.freeze
|
48
49
|
end
|
49
50
|
|
50
51
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Berater
|
4
|
+
module Middleware
|
5
|
+
class FailOpen
|
6
|
+
ERRORS = Set[
|
7
|
+
Redis::BaseConnectionError,
|
8
|
+
]
|
9
|
+
|
10
|
+
def initialize(errors: nil, on_fail: nil)
|
11
|
+
@errors = errors || ERRORS
|
12
|
+
@on_fail = on_fail
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(*, **opts)
|
16
|
+
yield.tap do |lock|
|
17
|
+
# wrap lock.release so it fails open
|
18
|
+
|
19
|
+
# save reference to original function
|
20
|
+
release_fn = lock.method(:release)
|
21
|
+
|
22
|
+
# make bound variables accessible to block
|
23
|
+
errors = @errors
|
24
|
+
on_fail = @on_fail
|
25
|
+
|
26
|
+
lock.define_singleton_method(:release) do
|
27
|
+
release_fn.call
|
28
|
+
rescue *errors => e
|
29
|
+
on_fail&.call(e)
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
rescue *@errors => e
|
34
|
+
@on_fail&.call(e)
|
35
|
+
|
36
|
+
# fail open by faking a lock
|
37
|
+
Berater::Lock.new(opts[:capacity], -1)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Berater
|
2
|
+
module Middleware
|
3
|
+
class LoadShedder
|
4
|
+
PRIORITY_RANGE = 1..5
|
5
|
+
|
6
|
+
def initialize(default_priority: nil)
|
7
|
+
@default_priority = default_priority
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(*args, **opts)
|
11
|
+
if priority = opts.delete(:priority) || @default_priority
|
12
|
+
opts[:capacity] = adjust_capacity(opts[:capacity], priority)
|
13
|
+
end
|
14
|
+
|
15
|
+
yield *args, **opts
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def adjust_capacity(capacity, priority)
|
21
|
+
unless PRIORITY_RANGE.include?(priority)
|
22
|
+
return capacity
|
23
|
+
end
|
24
|
+
|
25
|
+
# priority 1 stays at 100%, 2 scales down to 90%, 5 to 60%
|
26
|
+
factor = 1 - (priority - 1) * 0.1
|
27
|
+
(capacity * factor).floor
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/berater/utils.rb
CHANGED
data/lib/berater/version.rb
CHANGED
data/lib/berater.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
require 'berater/limiter'
|
2
|
+
require 'berater/limiter_set'
|
2
3
|
require 'berater/lock'
|
3
4
|
require 'berater/lua_script'
|
5
|
+
require 'berater/middleware'
|
4
6
|
require 'berater/utils'
|
5
7
|
require 'berater/version'
|
8
|
+
require 'meddleware'
|
6
9
|
|
7
10
|
module Berater
|
8
11
|
extend self
|
@@ -15,8 +18,14 @@ module Berater
|
|
15
18
|
yield self
|
16
19
|
end
|
17
20
|
|
18
|
-
def
|
19
|
-
@
|
21
|
+
def limiters
|
22
|
+
@limiters ||= LimiterSet.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def middleware(&block)
|
26
|
+
(@middleware ||= Meddleware.new).tap do
|
27
|
+
@middleware.instance_eval(&block) if block_given?
|
28
|
+
end
|
20
29
|
end
|
21
30
|
|
22
31
|
def new(key, capacity, **opts)
|
@@ -48,6 +57,11 @@ module Berater
|
|
48
57
|
end
|
49
58
|
end
|
50
59
|
|
60
|
+
def reset
|
61
|
+
@redis = nil
|
62
|
+
limiters.clear
|
63
|
+
middleware.clear
|
64
|
+
end
|
51
65
|
end
|
52
66
|
|
53
67
|
# convenience method
|
data/spec/berater_spec.rb
CHANGED
@@ -24,6 +24,24 @@ describe Berater do
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
describe '.limiters' do
|
28
|
+
subject { Berater.limiters }
|
29
|
+
|
30
|
+
let(:limiter) { Berater(:key, 1) }
|
31
|
+
|
32
|
+
it 'provides access to predefined limiters' do
|
33
|
+
expect(Berater.limiters).to be_a Berater::LimiterSet
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'resets with Berater' do
|
37
|
+
subject << limiter
|
38
|
+
is_expected.not_to be_empty
|
39
|
+
|
40
|
+
Berater.reset
|
41
|
+
is_expected.to be_empty
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
27
45
|
shared_examples 'a Berater' do |klass, capacity, **opts|
|
28
46
|
describe '.new' do
|
29
47
|
let(:limiter) { Berater.new(:key, capacity, **opts) }
|
@@ -41,8 +59,8 @@ describe Berater do
|
|
41
59
|
end
|
42
60
|
|
43
61
|
it 'accepts an optional redis parameter' do
|
44
|
-
redis = double(
|
45
|
-
limiter = Berater.new(:key, capacity, opts.merge(redis: redis))
|
62
|
+
redis = double(Redis)
|
63
|
+
limiter = Berater.new(:key, capacity, **opts.merge(redis: redis))
|
46
64
|
expect(limiter.redis).to be redis
|
47
65
|
end
|
48
66
|
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
describe Berater::LimiterSet do
|
2
|
+
subject { described_class.new }
|
3
|
+
|
4
|
+
let(:unlimiter) { Berater::Unlimiter.new }
|
5
|
+
let(:inhibitor) { Berater::Inhibitor.new }
|
6
|
+
|
7
|
+
describe '#each' do
|
8
|
+
it 'returns an Enumerator' do
|
9
|
+
expect(subject.each).to be_a Enumerator
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'works with an empty set' do
|
13
|
+
expect(subject.each.to_a).to eq []
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns elements' do
|
17
|
+
subject << unlimiter
|
18
|
+
expect(subject.each.to_a).to eq [ unlimiter ]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#<<' do
|
23
|
+
it 'adds a limiter' do
|
24
|
+
subject << unlimiter
|
25
|
+
expect(subject.each.to_a).to eq [ unlimiter ]
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'rejects things that are not limiters' do
|
29
|
+
expect {
|
30
|
+
subject << :foo
|
31
|
+
}.to raise_error(ArgumentError)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'updates existing keys' do
|
35
|
+
limiter = Berater::Unlimiter.new
|
36
|
+
expect(limiter).to eq unlimiter
|
37
|
+
expect(limiter).not_to be unlimiter
|
38
|
+
|
39
|
+
subject << unlimiter
|
40
|
+
subject << limiter
|
41
|
+
|
42
|
+
expect(subject.each.to_a).to eq [ limiter ]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '[]=' do
|
47
|
+
it 'adds a limiter' do
|
48
|
+
subject[:key] = unlimiter
|
49
|
+
|
50
|
+
expect(subject.each.to_a).to eq [ unlimiter ]
|
51
|
+
is_expected.to include :key
|
52
|
+
is_expected.to include unlimiter
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'rejects things that are not limiters' do
|
56
|
+
expect {
|
57
|
+
subject[:key] = :foo
|
58
|
+
}.to raise_error(ArgumentError)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#[]' do
|
63
|
+
it 'returns nil for missing keys' do
|
64
|
+
expect(subject[:key]).to be nil
|
65
|
+
expect(subject[nil]).to be nil
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'retreives limiters' do
|
69
|
+
subject << unlimiter
|
70
|
+
expect(subject[unlimiter.key]).to be unlimiter
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#fetch' do
|
75
|
+
it 'raises for missing keys' do
|
76
|
+
expect {
|
77
|
+
subject.fetch(:key)
|
78
|
+
}.to raise_error(KeyError)
|
79
|
+
|
80
|
+
expect {
|
81
|
+
subject.fetch(nil)
|
82
|
+
}.to raise_error(KeyError)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'returns the default if provided' do
|
86
|
+
expect(subject.fetch(:key, unlimiter)).to be unlimiter
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'calls the default proc if provided' do
|
90
|
+
expect {|block| subject.fetch(:key, &block) }.to yield_control
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'retreives limiters' do
|
94
|
+
subject << unlimiter
|
95
|
+
expect(subject.fetch(unlimiter.key)).to be unlimiter
|
96
|
+
expect(subject.fetch(unlimiter.key, :default)).to be unlimiter
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#include?' do
|
101
|
+
before do
|
102
|
+
subject << unlimiter
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'works with keys' do
|
106
|
+
is_expected.to include unlimiter.key
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'works with limiters' do
|
110
|
+
is_expected.to include unlimiter
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'works when target is missing' do
|
114
|
+
is_expected.not_to include inhibitor.key
|
115
|
+
is_expected.not_to include inhibitor
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe '#clear' do
|
120
|
+
it 'works when empty' do
|
121
|
+
subject.clear
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'clears limiters' do
|
125
|
+
subject << unlimiter
|
126
|
+
is_expected.to include unlimiter
|
127
|
+
|
128
|
+
subject.clear
|
129
|
+
is_expected.not_to include unlimiter
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe '#count' do
|
134
|
+
it 'counts' do
|
135
|
+
expect(subject.count).to be 0
|
136
|
+
|
137
|
+
subject << unlimiter
|
138
|
+
expect(subject.count).to be 1
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe '#delete' do
|
143
|
+
it 'works when the target is missing' do
|
144
|
+
subject.delete(unlimiter)
|
145
|
+
subject.delete(unlimiter.key)
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'works with keys' do
|
149
|
+
subject << unlimiter
|
150
|
+
is_expected.to include unlimiter
|
151
|
+
|
152
|
+
subject.delete(unlimiter.key)
|
153
|
+
is_expected.not_to include unlimiter
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'works with limiters' do
|
157
|
+
subject << unlimiter
|
158
|
+
is_expected.to include unlimiter
|
159
|
+
|
160
|
+
subject.delete(unlimiter)
|
161
|
+
is_expected.not_to include unlimiter
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe '#empty?' do
|
166
|
+
it 'works' do
|
167
|
+
is_expected.to be_empty
|
168
|
+
|
169
|
+
subject << unlimiter
|
170
|
+
is_expected.not_to be_empty
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/spec/limiter_spec.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
describe Berater::Limiter do
|
2
|
-
|
3
|
-
|
2
|
+
describe '.new' do
|
3
|
+
it 'can only be called on subclasses' do
|
4
|
+
expect { described_class.new }.to raise_error(NoMethodError)
|
5
|
+
end
|
4
6
|
end
|
5
7
|
|
6
8
|
describe 'abstract methods' do
|
@@ -50,6 +52,43 @@ describe Berater::Limiter do
|
|
50
52
|
}.to raise_error(ArgumentError)
|
51
53
|
end
|
52
54
|
end
|
55
|
+
|
56
|
+
context 'when Berater.redis is nil' do
|
57
|
+
let!(:redis) { Berater.redis }
|
58
|
+
|
59
|
+
before { Berater.redis = nil }
|
60
|
+
|
61
|
+
it 'works with Unlimiter since redis is not used' do
|
62
|
+
expect(subject.redis).to be nil
|
63
|
+
expect {|b| subject.limit(&b) }.to yield_control
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'raises when redis is needed' do
|
67
|
+
limiter = Berater::RateLimiter.new(:key, 1, :second)
|
68
|
+
expect(limiter.redis).to be nil
|
69
|
+
expect { limiter.limit }.to raise_error(RuntimeError)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'works when redis is passed in' do
|
73
|
+
limiter = Berater::RateLimiter.new(:key, 1, :second, redis: redis)
|
74
|
+
expect {|b| limiter.limit(&b) }.to yield_control
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'raises when redis is bogus' do
|
78
|
+
limiter = Berater::RateLimiter.new(:key, 1, :second, redis: :stub)
|
79
|
+
expect { limiter.limit }.to raise_error(RuntimeError)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'releases the lock even when limited code raises an error' do
|
84
|
+
lock = Berater::Lock.new(Float::INFINITY, 0)
|
85
|
+
expect(subject).to receive(:acquire_lock).and_return(lock)
|
86
|
+
expect(lock).to receive(:release)
|
87
|
+
|
88
|
+
expect {
|
89
|
+
subject.limit { raise 'fail' }
|
90
|
+
}.to raise_error(RuntimeError)
|
91
|
+
end
|
53
92
|
end
|
54
93
|
|
55
94
|
describe '#==' do
|
data/spec/lua_script_spec.rb
CHANGED
@@ -0,0 +1,184 @@
|
|
1
|
+
describe Berater::Middleware::FailOpen do
|
2
|
+
let(:limiter) { Berater::Unlimiter.new }
|
3
|
+
let(:lock) { limiter.limit }
|
4
|
+
let(:error) { Redis::TimeoutError }
|
5
|
+
|
6
|
+
describe '.call' do
|
7
|
+
let(:instance) { described_class.new(errors: errors, on_fail: on_fail) }
|
8
|
+
let(:errors) { nil }
|
9
|
+
let(:on_fail) { nil }
|
10
|
+
|
11
|
+
it 'returns the blocks value' do
|
12
|
+
expect(instance.call { lock }).to be lock
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'when there is an error during lock acquisition' do
|
16
|
+
subject { instance.call { raise error } }
|
17
|
+
|
18
|
+
it 'still returns a lock' do
|
19
|
+
expect(subject).to be_a Berater::Lock
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'creates a new, fake lock' do
|
23
|
+
expect(Berater::Lock).to receive(:new)
|
24
|
+
subject
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns a lock that is releasable' do
|
28
|
+
expect(subject.release).to be true
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when an on_fail handler is defined' do
|
32
|
+
let(:on_fail) { double(Proc) }
|
33
|
+
|
34
|
+
it 'calls the handler' do
|
35
|
+
expect(on_fail).to receive(:call).with(error)
|
36
|
+
subject
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when the error is an IOError' do
|
41
|
+
let(:error) { IOError }
|
42
|
+
|
43
|
+
it 'would normally not catch the error' do
|
44
|
+
expect { subject }.to raise_error(error)
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'and errors option is set' do
|
48
|
+
let(:errors) { [ error ] }
|
49
|
+
|
50
|
+
it 'catches the error' do
|
51
|
+
expect { subject }.not_to raise_error
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when there is an error during lock release' do
|
58
|
+
subject { instance.call { lock }.release }
|
59
|
+
|
60
|
+
before do
|
61
|
+
expect(lock).to receive(:release).and_raise(error)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'handles the exception' do
|
65
|
+
expect { subject }.not_to raise_error
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'returns false since lock was not released' do
|
69
|
+
is_expected.to be false
|
70
|
+
end
|
71
|
+
|
72
|
+
context 'when an on_fail handler is defined' do
|
73
|
+
let(:on_fail) { double(Proc) }
|
74
|
+
|
75
|
+
it 'calls the handler' do
|
76
|
+
expect(on_fail).to receive(:call).with(Exception)
|
77
|
+
subject
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'when the error is an IOError' do
|
82
|
+
let(:error) { IOError }
|
83
|
+
|
84
|
+
it 'would normally not catch the error' do
|
85
|
+
expect { subject }.to raise_error(error)
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'and errors option is set' do
|
89
|
+
let(:errors) { [ error ] }
|
90
|
+
|
91
|
+
it 'catches the error' do
|
92
|
+
expect { subject }.not_to raise_error
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'when there is an error during lock acquisition' do
|
100
|
+
before do
|
101
|
+
expect(limiter).to receive(:acquire_lock).and_raise(error)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'raises an exception for the caller' do
|
105
|
+
expect { limiter.limit }.to raise_error(error)
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'when FailOpen middleware is enabled' do
|
109
|
+
before do
|
110
|
+
Berater.middleware.use described_class
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'fails open' do
|
114
|
+
expect(limiter.limit).to be_a Berater::Lock
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'returns the intended result' do
|
118
|
+
expect(limiter.limit { 123 }).to be 123
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'when FailOpen middleware is enabled with callback' do
|
123
|
+
before do
|
124
|
+
Berater.middleware.use described_class, on_fail: on_fail
|
125
|
+
end
|
126
|
+
let(:on_fail) { double(Proc) }
|
127
|
+
|
128
|
+
it 'calls the callback' do
|
129
|
+
expect(on_fail).to receive(:call).with(Exception)
|
130
|
+
limiter.limit
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'when there is an error during lock release' do
|
136
|
+
before do
|
137
|
+
allow(limiter).to receive(:acquire_lock).and_return(lock)
|
138
|
+
allow(lock).to receive(:release).and_raise(error)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'acquires a lock' do
|
142
|
+
expect(limiter.limit).to be_a Berater::Lock
|
143
|
+
expect(limiter.limit).to be lock
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'raises an exception when lock is released' do
|
147
|
+
expect {
|
148
|
+
limiter.limit.release
|
149
|
+
}.to raise_error(error)
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'raises an exception when lock is auto released' do
|
153
|
+
expect {
|
154
|
+
limiter.limit {}
|
155
|
+
}.to raise_error(error)
|
156
|
+
end
|
157
|
+
|
158
|
+
context 'when FailOpen middleware is enabled' do
|
159
|
+
before do
|
160
|
+
Berater.middleware.use described_class
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'fails open' do
|
164
|
+
expect { limiter.limit.release }.not_to raise_error
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'returns the intended result' do
|
168
|
+
expect(limiter.limit { 123 }).to be 123
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'when FailOpen middleware is enabled with callback' do
|
173
|
+
before do
|
174
|
+
Berater.middleware.use described_class, on_fail: on_fail
|
175
|
+
end
|
176
|
+
let(:on_fail) { double(Proc) }
|
177
|
+
|
178
|
+
it 'calls the callback' do
|
179
|
+
expect(on_fail).to receive(:call).with(Exception)
|
180
|
+
limiter.limit {}
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
describe Berater::Middleware::LoadShedder do
|
2
|
+
describe '#call' do
|
3
|
+
subject { described_class.new }
|
4
|
+
|
5
|
+
before { Berater.test_mode = :pass }
|
6
|
+
|
7
|
+
it 'yields' do
|
8
|
+
expect {|b| subject.call(&b) }.to yield_control
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'passes through capacity and cost options' do
|
12
|
+
opts = {
|
13
|
+
capacity: 1,
|
14
|
+
cost: 2,
|
15
|
+
}
|
16
|
+
|
17
|
+
subject.call(**opts) do |**passed_opts|
|
18
|
+
expect(passed_opts).to eq(opts)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'strips out priority from options' do
|
23
|
+
opts = {
|
24
|
+
capacity: 1,
|
25
|
+
priority: 3,
|
26
|
+
}
|
27
|
+
|
28
|
+
subject.call(**opts) do |**passed_opts|
|
29
|
+
expect(passed_opts.keys).not_to include(:priority)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'keeps full capacity for priority 1' do
|
34
|
+
subject.call(capacity: 100, priority: 1) do |capacity:|
|
35
|
+
expect(capacity).to eq 100
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'adjusts the capactiy according to priority' do
|
40
|
+
subject.call(capacity: 100, priority: 2) do |capacity:|
|
41
|
+
expect(capacity).to be < 100
|
42
|
+
end
|
43
|
+
|
44
|
+
subject.call(capacity: 100, priority: 5) do |capacity:|
|
45
|
+
expect(capacity).to eq 60
|
46
|
+
end
|
47
|
+
end
|
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
|
+
it 'works with a fractional priority' do
|
64
|
+
subject.call(capacity: 100, priority: 1.5) do |capacity:|
|
65
|
+
expect(capacity).to be < 100
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'with a default priority' do
|
70
|
+
subject { described_class.new(default_priority: 5) }
|
71
|
+
|
72
|
+
it 'keeps full capacity for priority 1' do
|
73
|
+
subject.call(capacity: 100, priority: 1) do |capacity:|
|
74
|
+
expect(capacity).to eq 100
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'uses the default priority' do
|
79
|
+
subject.call(capacity: 100) do |capacity:|
|
80
|
+
expect(capacity).to eq 60
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'with a limiter' do
|
87
|
+
before do
|
88
|
+
Berater.middleware.use Berater::Middleware::LoadShedder
|
89
|
+
end
|
90
|
+
|
91
|
+
shared_examples 'limiter load shedding' do |limiter|
|
92
|
+
it 'passes through the capactiy properly' do
|
93
|
+
expect(limiter).to receive(:inner_limit).with(
|
94
|
+
hash_including(capacity: 100)
|
95
|
+
).and_call_original
|
96
|
+
|
97
|
+
limiter.limit
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'scales the capactiy with priority' do
|
101
|
+
expect(limiter).to receive(:inner_limit).with(
|
102
|
+
hash_including(capacity: 60)
|
103
|
+
).and_call_original
|
104
|
+
|
105
|
+
limiter.limit(priority: 5)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'overloads properly' do
|
109
|
+
60.times { limiter.limit(priority: 5) }
|
110
|
+
|
111
|
+
expect {
|
112
|
+
limiter.limit(priority: 5)
|
113
|
+
}.to be_overloaded
|
114
|
+
|
115
|
+
expect {
|
116
|
+
limiter.limit(priority: 4)
|
117
|
+
}.not_to be_overloaded
|
118
|
+
|
119
|
+
39.times { limiter.limit(priority: 1) }
|
120
|
+
|
121
|
+
expect {
|
122
|
+
limiter.limit(priority: 1)
|
123
|
+
}.to be_overloaded
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
include_examples 'limiter load shedding', Berater::ConcurrencyLimiter.new(:key, 100)
|
128
|
+
include_examples 'limiter load shedding', Berater::RateLimiter.new(:key, 100, :second)
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
class Meddler
|
2
|
+
def call(*args, **kwargs)
|
3
|
+
yield
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
describe 'Berater.middleware' do
|
8
|
+
subject { Berater.middleware }
|
9
|
+
|
10
|
+
describe 'adding middleware' do
|
11
|
+
after { is_expected.to include Meddler }
|
12
|
+
|
13
|
+
it 'can be done inline' do
|
14
|
+
Berater.middleware.use Meddler
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'can be done with a block' do
|
18
|
+
Berater.middleware do
|
19
|
+
use Meddler
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'resets along with Berater' do
|
25
|
+
Berater.middleware.use Meddler
|
26
|
+
is_expected.to include Meddler
|
27
|
+
|
28
|
+
Berater.reset
|
29
|
+
is_expected.to be_empty
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'Berater::Limiter#limit' do
|
33
|
+
let(:middleware) { Meddler.new }
|
34
|
+
let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
|
35
|
+
|
36
|
+
before do
|
37
|
+
expect(Meddler).to receive(:new).and_return(middleware).at_least(1)
|
38
|
+
Berater.middleware.use Meddler
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'calls the middleware' do
|
42
|
+
expect(middleware).to receive(:call)
|
43
|
+
limiter.limit
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'calls the middleware, passing the limiter and options' do
|
47
|
+
expect(middleware).to receive(:call).with(
|
48
|
+
limiter,
|
49
|
+
hash_including(:capacity, :cost)
|
50
|
+
)
|
51
|
+
|
52
|
+
limiter.limit
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when used per ususual' do
|
56
|
+
before do
|
57
|
+
expect(middleware).to receive(:call).and_call_original.at_least(1)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'still works inline' do
|
61
|
+
expect(limiter.limit).to be_a Berater::Lock
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'still works in block mode' do
|
65
|
+
expect(limiter.limit { 123 }).to be 123
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'still has limits' do
|
69
|
+
limiter.limit
|
70
|
+
expect(limiter).to be_overloaded
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when middleware meddles' do
|
75
|
+
it 'can change the capacity' do
|
76
|
+
expect(middleware).to receive(:call) do |limiter, **opts, &block|
|
77
|
+
opts[:capacity] = 0
|
78
|
+
block.call(limiter, **opts)
|
79
|
+
end
|
80
|
+
|
81
|
+
expect { limiter.limit }.to be_overloaded
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'can change the cost' do
|
85
|
+
expect(middleware).to receive(:call) do |limiter, **opts, &block|
|
86
|
+
opts[:cost] = 2
|
87
|
+
block.call(limiter, **opts)
|
88
|
+
end
|
89
|
+
|
90
|
+
expect { limiter.limit }.to be_overloaded
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'can change the limiter' do
|
94
|
+
other_limiter = Berater::Inhibitor.new
|
95
|
+
|
96
|
+
expect(middleware).to receive(:call) do |limiter, **opts, &block|
|
97
|
+
block.call(other_limiter, **opts)
|
98
|
+
end
|
99
|
+
expect(other_limiter).to receive(:acquire_lock).and_call_original
|
100
|
+
|
101
|
+
expect { limiter.limit }.to be_overloaded
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'can abort by not yielding' do
|
105
|
+
expect(middleware).to receive(:call)
|
106
|
+
expect(limiter.limit).to be nil
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'can intercept the lock' do
|
110
|
+
expect(middleware).to receive(:call) do |&block|
|
111
|
+
lock = block.call
|
112
|
+
expect(lock).to be_a Berater::Lock
|
113
|
+
expect(lock.capacity).to eq limiter.capacity
|
114
|
+
end
|
115
|
+
|
116
|
+
limiter.limit
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: berater
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.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-
|
11
|
+
date: 2021-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: meddleware
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.2'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: redis
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
16
30
|
requirements:
|
17
31
|
- - ">="
|
18
32
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
33
|
+
version: '3'
|
20
34
|
type: :runtime
|
21
35
|
prerelease: false
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
23
37
|
requirements:
|
24
38
|
- - ">="
|
25
39
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
40
|
+
version: '3'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: benchmark
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -133,8 +147,12 @@ files:
|
|
133
147
|
- lib/berater/dsl.rb
|
134
148
|
- lib/berater/inhibitor.rb
|
135
149
|
- lib/berater/limiter.rb
|
150
|
+
- lib/berater/limiter_set.rb
|
136
151
|
- lib/berater/lock.rb
|
137
152
|
- lib/berater/lua_script.rb
|
153
|
+
- lib/berater/middleware.rb
|
154
|
+
- lib/berater/middleware/fail_open.rb
|
155
|
+
- lib/berater/middleware/load_shedder.rb
|
138
156
|
- lib/berater/rate_limiter.rb
|
139
157
|
- lib/berater/rspec.rb
|
140
158
|
- lib/berater/rspec/matchers.rb
|
@@ -148,9 +166,13 @@ files:
|
|
148
166
|
- spec/dsl_refinement_spec.rb
|
149
167
|
- spec/dsl_spec.rb
|
150
168
|
- spec/inhibitor_spec.rb
|
169
|
+
- spec/limiter_set_spec.rb
|
151
170
|
- spec/limiter_spec.rb
|
152
171
|
- spec/lua_script_spec.rb
|
153
172
|
- spec/matchers_spec.rb
|
173
|
+
- spec/middleware/fail_open_spec.rb
|
174
|
+
- spec/middleware/load_shedder_spec.rb
|
175
|
+
- spec/middleware_spec.rb
|
154
176
|
- spec/rate_limiter_spec.rb
|
155
177
|
- spec/riddle_spec.rb
|
156
178
|
- spec/static_limiter_spec.rb
|
@@ -176,19 +198,23 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
176
198
|
- !ruby/object:Gem::Version
|
177
199
|
version: '0'
|
178
200
|
requirements: []
|
179
|
-
rubygems_version: 3.
|
201
|
+
rubygems_version: 3.1.6
|
180
202
|
signing_key:
|
181
203
|
specification_version: 4
|
182
204
|
summary: Berater
|
183
205
|
test_files:
|
184
206
|
- spec/rate_limiter_spec.rb
|
207
|
+
- spec/middleware/load_shedder_spec.rb
|
208
|
+
- spec/middleware/fail_open_spec.rb
|
185
209
|
- spec/matchers_spec.rb
|
186
210
|
- spec/dsl_refinement_spec.rb
|
187
211
|
- spec/test_mode_spec.rb
|
212
|
+
- spec/middleware_spec.rb
|
188
213
|
- spec/dsl_spec.rb
|
189
214
|
- spec/lua_script_spec.rb
|
190
215
|
- spec/concurrency_limiter_spec.rb
|
191
216
|
- spec/riddle_spec.rb
|
217
|
+
- spec/limiter_set_spec.rb
|
192
218
|
- spec/utils_spec.rb
|
193
219
|
- spec/berater_spec.rb
|
194
220
|
- spec/limiter_spec.rb
|