berater 0.8.0 → 0.9.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: 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