berater 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c32c1de752ccc7a2a71e4628b99f3561db623374dabaf95af39aadcc3b1a2d16
4
- data.tar.gz: 33019954de1d79522ed7105e9f0a7da3a204df74cacd25aa4b57e749a81047bb
3
+ metadata.gz: 476b59e3f1e27908f5c8a5097087e6c8c2384531e1284d79b9d89a73fbbb5840
4
+ data.tar.gz: de50fdcab6ea7dc9520fcf1aefdd07b1f8775c0ce96a30e3f1d45b6ccfb7b00f
5
5
  SHA512:
6
- metadata.gz: 89460a9a81a8d384addb095c425da0eed4fdbe0e15ecd85ee97ffcf4292441deeb34c7e4cd4591adca06096a17f7c131b79c6474d9efb2b635fbe398ad68d00e
7
- data.tar.gz: 0a331b939e4c19817c42a8aa8fc5be7fd59d63f6becd5fe6ed1c226690d1524ef040a28d8bcb625d75452ab2f5c8539ddf69093bbd30c4ba9e3353259a3e568d
6
+ metadata.gz: 2446e64f528a8ef1d37791922d37cddcebdf702f3c5beff2ef7ea9d611ce4cc6d6c40ac42e1ea826987a2eb2ceea3ddf97e420bb7369686b8239b260a1725c55
7
+ data.tar.gz: 23dccad7e02cc425631ceeb1dfbc6960b94ac92c523d840a991917af02caaf39a7befc5887fe45631f565ddb2eddb4850e557348ab29427d3cbae1db89124c3c
data/lib/berater.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require 'berater/limiter'
2
+ require 'berater/limiter_set'
2
3
  require 'berater/lock'
3
4
  require 'berater/lua_script'
4
5
  require 'berater/utils'
5
6
  require 'berater/version'
7
+ require 'meddleware'
6
8
 
7
9
  module Berater
8
10
  extend self
@@ -15,8 +17,14 @@ module Berater
15
17
  yield self
16
18
  end
17
19
 
18
- def reset
19
- @redis = nil
20
+ def limiters
21
+ @limiters ||= LimiterSet.new
22
+ end
23
+
24
+ def middleware(&block)
25
+ (@middleware ||= Meddleware.new).tap do
26
+ @middleware.instance_eval(&block) if block_given?
27
+ end
20
28
  end
21
29
 
22
30
  def new(key, capacity, **opts)
@@ -48,6 +56,11 @@ module Berater
48
56
  end
49
57
  end
50
58
 
59
+ def reset
60
+ @redis = nil
61
+ limiters.clear
62
+ middleware.clear
63
+ end
51
64
  end
52
65
 
53
66
  # convenience method
@@ -9,17 +9,12 @@ module Berater
9
9
 
10
10
  def limit(capacity: nil, cost: 1, &block)
11
11
  capacity ||= @capacity
12
+ lock = nil
12
13
 
13
- unless capacity.is_a?(Numeric) && capacity >= 0
14
- raise ArgumentError, "invalid capacity: #{capacity}"
14
+ Berater.middleware.call(self, capacity: capacity, cost: cost) do |limiter, **opts|
15
+ lock = limiter.inner_limit(**opts)
15
16
  end
16
17
 
17
- unless cost.is_a?(Numeric) && cost >= 0 && cost < Float::INFINITY
18
- raise ArgumentError, "invalid cost: #{cost}"
19
- end
20
-
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,27 @@ 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(*)
103
+ # can only call via subclass
104
+ raise NoMethodError if self == Berater::Limiter
105
+
106
+ super
107
+ end
100
108
 
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)
109
+ def cache_key(key)
110
+ klass = to_s.split(':')[-1]
111
+ "Berater:#{klass}:#{key}"
112
+ end
113
+
114
+ protected
115
+
116
+ def inherited(subclass)
117
+ # automagically create convenience method
118
+ name = subclass.to_s.split(':')[-1]
119
+ Berater.define_singleton_method(name) do |*args, **opts, &block|
120
+ Berater::Utils.convenience_fn(subclass, *args, **opts, &block)
121
+ end
106
122
  end
107
123
  end
108
124
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.8.0'
2
+ VERSION = '0.9.0'
3
3
  end
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) }
@@ -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,33 @@ 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
53
82
  end
54
83
 
55
84
  describe '#==' do
@@ -0,0 +1,110 @@
1
+ class Meddler
2
+ def call(*)
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
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
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
+ end
109
+ end
110
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
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.9.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-04-09 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'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: redis
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -133,6 +147,7 @@ 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
138
153
  - lib/berater/rate_limiter.rb
@@ -148,9 +163,11 @@ files:
148
163
  - spec/dsl_refinement_spec.rb
149
164
  - spec/dsl_spec.rb
150
165
  - spec/inhibitor_spec.rb
166
+ - spec/limiter_set_spec.rb
151
167
  - spec/limiter_spec.rb
152
168
  - spec/lua_script_spec.rb
153
169
  - spec/matchers_spec.rb
170
+ - spec/middleware_spec.rb
154
171
  - spec/rate_limiter_spec.rb
155
172
  - spec/riddle_spec.rb
156
173
  - spec/static_limiter_spec.rb
@@ -185,10 +202,12 @@ test_files:
185
202
  - spec/matchers_spec.rb
186
203
  - spec/dsl_refinement_spec.rb
187
204
  - spec/test_mode_spec.rb
205
+ - spec/middleware_spec.rb
188
206
  - spec/dsl_spec.rb
189
207
  - spec/lua_script_spec.rb
190
208
  - spec/concurrency_limiter_spec.rb
191
209
  - spec/riddle_spec.rb
210
+ - spec/limiter_set_spec.rb
192
211
  - spec/utils_spec.rb
193
212
  - spec/berater_spec.rb
194
213
  - spec/limiter_spec.rb