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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c32c1de752ccc7a2a71e4628b99f3561db623374dabaf95af39aadcc3b1a2d16
4
- data.tar.gz: 33019954de1d79522ed7105e9f0a7da3a204df74cacd25aa4b57e749a81047bb
3
+ metadata.gz: f57ab651eeb34e6a0bb7b889fefa9c13d1d1fc5b6e834bc2b9a6e0ba7b74a8e2
4
+ data.tar.gz: 8112aa91ae48872132d8222a07f03dce42ff6f5ff26a3bf17906a389ba675ec9
5
5
  SHA512:
6
- metadata.gz: 89460a9a81a8d384addb095c425da0eed4fdbe0e15ecd85ee97ffcf4292441deeb34c7e4cd4591adca06096a17f7c131b79c6474d9efb2b635fbe398ad68d00e
7
- data.tar.gz: 0a331b939e4c19817c42a8aa8fc5be7fd59d63f6becd5fe6ed1c226690d1524ef040a28d8bcb625d75452ab2f5c8539ddf69093bbd30c4ba9e3353259a3e568d
6
+ metadata.gz: 17ae5da54dcf9535d4afe93d7e239ddc1fefaedf3419883cfa22d1f55c0c0aa7a9365986509e8e54e37c08c258068db4a76064dcc6af3f654e170b3efc8c1cd0
7
+ data.tar.gz: 04b1e7553e86107bbc91fc486b7b0ae099f5575b8654f7800f2a1c281b7b8bfde1e63df2378f03fcb4a9864b15b6a5de610bb6e7a1ce4d4b8e53898d9f4f0869
@@ -7,19 +7,14 @@ module Berater
7
7
  options[:redis] || Berater.redis
8
8
  end
9
9
 
10
- def limit(capacity: nil, cost: 1, &block)
11
- capacity ||= @capacity
10
+ def limit(**opts, &block)
11
+ opts[:capacity] ||= @capacity
12
+ opts[:cost] ||= 1
12
13
 
13
- unless capacity.is_a?(Numeric) && capacity >= 0
14
- raise ArgumentError, "invalid capacity: #{capacity}"
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
- def self.cache_key(key)
97
- klass = to_s.split(':')[-1]
98
- "Berater:#{klass}:#{key}"
99
- end
101
+ class << self
102
+ def new(*args, **kwargs)
103
+ # can only call via subclass
104
+ raise NoMethodError if self == Berater::Limiter
100
105
 
101
- def self.inherited(subclass)
102
- # automagically create convenience method
103
- name = subclass.to_s.split(':')[-1]
104
- Berater.define_singleton_method(name) do |*args, **opts, &block|
105
- Berater::Utils.convenience_fn(subclass, *args, **opts, &block)
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
@@ -20,7 +20,7 @@ module Berater
20
20
 
21
21
  @released_at = Time.now
22
22
  @release_fn&.call
23
- nil
23
+ true
24
24
  end
25
25
 
26
26
  end
@@ -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
@@ -0,0 +1,6 @@
1
+ module Berater
2
+ module Middleware
3
+ autoload 'FailOpen', 'berater/middleware/fail_open'
4
+ autoload 'LoadShedder', 'berater/middleware/load_shedder'
5
+ end
6
+ end
data/lib/berater/utils.rb CHANGED
@@ -44,12 +44,7 @@ module Berater
44
44
 
45
45
  def convenience_fn(klass, *args, **opts, &block)
46
46
  limiter = klass.new(*args, **opts)
47
- if block_given?
48
- limiter.limit(&block)
49
- else
50
- limiter
51
- end
47
+ block ? limiter.limit(&block) : limiter
52
48
  end
53
-
54
49
  end
55
50
  end
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.8.0'
2
+ VERSION = "0.11.0"
3
3
  end
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 reset
19
- @redis = nil
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('Redis')
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
- it 'can not be initialized' do
3
- expect { described_class.new }.to raise_error(NoMethodError)
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
@@ -93,5 +93,4 @@ describe Berater::LuaScript do
93
93
  it { subject }
94
94
  end
95
95
  end
96
-
97
96
  end
@@ -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.8.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-03-22 00:00:00.000000000 Z
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: '0'
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: '0'
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.0.8
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