berater 0.6.1 → 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.
data/lib/berater/rspec.rb CHANGED
@@ -1,14 +1,13 @@
1
1
  require 'berater'
2
2
  require 'berater/rspec/matchers'
3
3
  require 'berater/test_mode'
4
- require 'rspec'
4
+ require 'rspec/core'
5
5
 
6
6
  RSpec.configure do |config|
7
7
  config.include(Berater::Matchers)
8
8
 
9
9
  config.after do
10
10
  Berater.expunge rescue nil
11
- Berater.redis.script(:flush) rescue nil
12
11
  Berater.reset
13
12
  end
14
13
  end
@@ -1,10 +1,6 @@
1
1
  module Berater
2
2
  module Matchers
3
3
  class Overloaded
4
- def initialize(type)
5
- @type = type
6
- end
7
-
8
4
  def supports_block_expectations?
9
5
  true
10
6
  end
@@ -12,13 +8,13 @@ module Berater
12
8
  def matches?(obj)
13
9
  case obj
14
10
  when Proc
15
- # eg. expect { ... }.to be_overrated
11
+ # eg. expect { ... }.to be_overloaded
16
12
  res = obj.call
17
13
 
18
14
  if res.is_a? Berater::Limiter
19
15
  # eg. expect { Berater.new(...) }.to be_overloaded
20
16
  @limiter = res
21
- res.overloaded?
17
+ @limiter.utilization >= 1
22
18
  else
23
19
  # eg. expect { Berater(...) }.to be_overloaded
24
20
  # eg. expect { limiter.limit }.to be_overloaded
@@ -27,55 +23,39 @@ module Berater
27
23
  when Berater::Limiter
28
24
  # eg. expect(Berater.new(...)).to be_overloaded
29
25
  @limiter = obj
30
- obj.overloaded?
26
+ @limiter.utilization >= 1
31
27
  end
32
- rescue @type
28
+ rescue Berater::Overloaded
33
29
  true
34
30
  end
35
31
 
36
32
  def description
37
33
  if @limiter
38
- "be #{verb}"
34
+ "be overloaded"
39
35
  else
40
- "raise #{@type}"
36
+ "raise #{Berater::Overloaded}"
41
37
  end
42
38
  end
43
39
 
44
40
  def failure_message
45
41
  if @limiter
46
- "expected to be #{verb}"
42
+ "expected to be overloaded"
47
43
  else
48
- "expected #{@type} to be raised"
44
+ "expected #{Berater::Overloaded} to be raised"
49
45
  end
50
46
  end
51
47
 
52
48
  def failure_message_when_negated
53
49
  if @limiter
54
- "expected not to be #{verb}"
50
+ "expected not to be overloaded"
55
51
  else
56
- "did not expect #{@type} to be raised"
52
+ "did not expect #{Berater::Overloaded} to be raised"
57
53
  end
58
54
  end
59
-
60
- private def verb
61
- @type.to_s.split('::')[-1].downcase
62
- end
63
55
  end
64
56
 
65
57
  def be_overloaded
66
- Overloaded.new(Berater::Overloaded)
67
- end
68
-
69
- def be_overrated
70
- Overloaded.new(Berater::RateLimiter::Overrated)
71
- end
72
-
73
- def be_incapacitated
74
- Overloaded.new(Berater::ConcurrencyLimiter::Incapacitated)
75
- end
76
-
77
- def be_inhibited
78
- Overloaded.new(Berater::Inhibitor::Inhibited)
58
+ Overloaded.new
79
59
  end
80
60
  end
81
61
  end
@@ -0,0 +1,49 @@
1
+ module Berater
2
+ class StaticLimiter < Limiter
3
+
4
+ LUA_SCRIPT = Berater::LuaScript(<<~LUA
5
+ local key = KEYS[1]
6
+ local capacity = tonumber(ARGV[1])
7
+ local cost = tonumber(ARGV[2])
8
+
9
+ local count = redis.call('GET', key) or 0
10
+ local allowed = (count + cost) <= capacity
11
+
12
+ if allowed then
13
+ count = count + cost
14
+ redis.call('SET', key, count)
15
+ end
16
+
17
+ return { tostring(count), allowed }
18
+ LUA
19
+ )
20
+
21
+ protected def acquire_lock(capacity, cost)
22
+ if cost == 0
23
+ # utilization check
24
+ count = redis.get(cache_key) || "0"
25
+ allowed = true
26
+ else
27
+ count, allowed = LUA_SCRIPT.eval(
28
+ redis, [ cache_key ], [ capacity, cost ],
29
+ )
30
+ end
31
+
32
+ # Redis returns Floats as strings to maintain precision
33
+ count = count.include?('.') ? count.to_f : count.to_i
34
+
35
+ raise Overloaded unless allowed
36
+
37
+ release_fn = if cost > 0
38
+ proc { redis.incrbyfloat(cache_key, -cost) }
39
+ end
40
+
41
+ Lock.new(capacity, count, release_fn)
42
+ end
43
+
44
+ def to_s
45
+ "#<#{self.class}(#{key}: #{capacity})>"
46
+ end
47
+
48
+ end
49
+ end
@@ -1,43 +1,47 @@
1
1
  require 'berater'
2
2
 
3
3
  module Berater
4
- extend self
5
4
 
6
- attr_reader :test_mode
5
+ module TestMode
6
+ attr_reader :test_mode
7
+
8
+ def test_mode=(mode)
9
+ unless [ nil, :pass, :fail ].include?(mode)
10
+ raise ArgumentError, "invalid mode: #{Berater.test_mode}"
11
+ end
7
12
 
8
- def test_mode=(mode)
9
- unless [ nil, :pass, :fail ].include?(mode)
10
- raise ArgumentError, "invalid mode: #{Berater.test_mode}"
13
+ @test_mode = mode
11
14
  end
12
15
 
13
- @test_mode = mode
16
+ def reset
17
+ super
18
+ @test_mode = nil
19
+ end
14
20
  end
15
21
 
16
- module TestMode
17
- def acquire_lock(*)
18
- case Berater.test_mode
19
- when :pass
20
- Lock.new(Float::INFINITY, 0)
21
- when :fail
22
- # find class specific Overloaded error
23
- e = self.class.constants.map do |name|
24
- self.class.const_get(name)
25
- end.find do |const|
26
- const < Berater::Overloaded
27
- end || Berater::Overloaded
28
-
29
- raise e
30
- else
31
- super
22
+ class Limiter
23
+ module TestMode
24
+ def acquire_lock(*)
25
+ case Berater.test_mode
26
+ when :pass
27
+ Lock.new(Float::INFINITY, 0)
28
+ when :fail
29
+ raise Overloaded
30
+ else
31
+ super
32
+ end
32
33
  end
33
34
  end
34
35
  end
35
36
 
36
37
  end
37
38
 
39
+ # prepend class methods
40
+ Berater.singleton_class.prepend Berater::TestMode
41
+
38
42
  # stub each Limiter subclass
39
43
  ObjectSpace.each_object(Class).each do |klass|
40
44
  next unless klass < Berater::Limiter
41
45
 
42
- klass.prepend(Berater::TestMode)
46
+ klass.prepend Berater::Limiter::TestMode
43
47
  end
@@ -5,6 +5,10 @@ module Berater
5
5
  super(key, Float::INFINITY, **opts)
6
6
  end
7
7
 
8
+ def to_s
9
+ "#<#{self.class}>"
10
+ end
11
+
8
12
  protected
9
13
 
10
14
  def capacity=(*)
data/lib/berater/utils.rb CHANGED
@@ -42,5 +42,14 @@ module Berater
42
42
  (res * 10**3).to_i
43
43
  end
44
44
 
45
+ def convenience_fn(klass, *args, **opts, &block)
46
+ limiter = klass.new(*args, **opts)
47
+ if block_given?
48
+ limiter.limit(&block)
49
+ else
50
+ limiter
51
+ end
52
+ end
53
+
45
54
  end
46
55
  end
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.6.1'
2
+ VERSION = '0.9.0'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -24,50 +24,33 @@ describe Berater do
24
24
  end
25
25
  end
26
26
 
27
- describe '.new' do
28
- context 'unlimited mode' do
29
- let(:limiter) { Berater.new(:key, Float::INFINITY) }
27
+ describe '.limiters' do
28
+ subject { Berater.limiters }
30
29
 
31
- it 'instantiates an Unlimiter' do
32
- expect(limiter).to be_a Berater::Unlimiter
33
- expect(limiter.key).to be :key
34
- end
35
-
36
- it 'inherits redis' do
37
- expect(limiter.redis).to be Berater.redis
38
- end
30
+ let(:limiter) { Berater(:key, 1) }
39
31
 
40
- it 'accepts options' do
41
- redis = double('Redis')
42
- limiter = Berater.new(:key, Float::INFINITY, redis: redis)
43
- expect(limiter.redis).to be redis
44
- end
32
+ it 'provides access to predefined limiters' do
33
+ expect(Berater.limiters).to be_a Berater::LimiterSet
45
34
  end
46
35
 
47
- context 'inhibited mode' do
48
- let(:limiter) { Berater.new(:key, 0) }
36
+ it 'resets with Berater' do
37
+ subject << limiter
38
+ is_expected.not_to be_empty
49
39
 
50
- it 'instantiates an Inhibitor' do
51
- expect(limiter).to be_a Berater::Inhibitor
52
- expect(limiter.key).to be :key
53
- end
40
+ Berater.reset
41
+ is_expected.to be_empty
42
+ end
43
+ end
54
44
 
55
- it 'inherits redis' do
56
- expect(limiter.redis).to be Berater.redis
57
- end
45
+ shared_examples 'a Berater' do |klass, capacity, **opts|
46
+ describe '.new' do
47
+ let(:limiter) { Berater.new(:key, capacity, **opts) }
58
48
 
59
- it 'accepts options' do
60
- redis = double('Redis')
61
- limiter = Berater.new(:key, 0, redis: redis)
62
- expect(limiter.redis).to be redis
49
+ it 'instantiates the right class' do
50
+ expect(limiter).to be_a klass
63
51
  end
64
- end
65
-
66
- context 'rate mode' do
67
- let(:limiter) { Berater.new(:key, 1, :second) }
68
52
 
69
- it 'instantiates a RateLimiter' do
70
- expect(limiter).to be_a Berater::RateLimiter
53
+ it 'sets the key' do
71
54
  expect(limiter.key).to be :key
72
55
  end
73
56
 
@@ -75,60 +58,45 @@ describe Berater do
75
58
  expect(limiter.redis).to be Berater.redis
76
59
  end
77
60
 
78
- it 'accepts options' do
61
+ it 'accepts an optional redis parameter' do
79
62
  redis = double('Redis')
80
- limiter = Berater.new(:key, 1, :second, redis: redis)
63
+ limiter = Berater.new(:key, capacity, opts.merge(redis: redis))
81
64
  expect(limiter.redis).to be redis
82
65
  end
83
66
  end
84
67
 
85
- context 'concurrency mode' do
86
- let(:limiter) { Berater.new(:key, 1) }
68
+ describe 'Berater() convenience method' do
69
+ let(:limiter) { Berater(:key, capacity, **opts) }
87
70
 
88
- it 'instantiates a ConcurrencyLimiter' do
89
- expect(limiter).to be_a Berater::ConcurrencyLimiter
90
- expect(limiter.key).to be :key
71
+ it 'creates a limiter' do
72
+ expect(limiter).to be_a klass
91
73
  end
92
74
 
93
- it 'inherits redis' do
94
- expect(limiter.redis).to be Berater.redis
75
+ it 'creates an equivalent limiter' do
76
+ expect(limiter).to eq Berater.new(:key, capacity, **opts)
95
77
  end
96
78
 
97
- it 'accepts options' do
98
- redis = double('Redis')
99
- limiter = Berater.new(:key, 1, redis: redis)
100
- expect(limiter.redis).to be redis
101
- end
102
- end
103
- end
79
+ context 'with a block' do
80
+ before { Berater.test_mode = :pass }
104
81
 
105
- describe 'Berater() - convenience method' do
106
- RSpec.shared_examples 'test convenience' do |klass, *args|
107
- it 'creates a limiter' do
108
- limiter = Berater(:key, *args)
109
- expect(limiter).to be_a klass
110
- end
82
+ subject { Berater(:key, capacity, **opts) { 123 } }
111
83
 
112
- context 'with a block' do
113
84
  it 'creates a limiter and calls limit' do
114
- limiter = Berater(:key, *args)
115
85
  expect(klass).to receive(:new).and_return(limiter)
116
86
  expect(limiter).to receive(:limit).and_call_original
87
+ subject
88
+ end
117
89
 
118
- begin
119
- res = Berater(:key, *args) { true }
120
- expect(res).to be true
121
- rescue Berater::Overloaded
122
- expect(klass).to be Berater::Inhibitor
123
- end
90
+ it 'yields' do
91
+ is_expected.to be 123
124
92
  end
125
93
  end
126
94
  end
127
-
128
- include_examples 'test convenience', Berater::Unlimiter, Float::INFINITY
129
- include_examples 'test convenience', Berater::Inhibitor, 0
130
- include_examples 'test convenience', Berater::RateLimiter, 1, :second
131
- include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
132
95
  end
133
96
 
97
+ include_examples 'a Berater', Berater::ConcurrencyLimiter, 1, timeout: 1
98
+ include_examples 'a Berater', Berater::Inhibitor, 0
99
+ include_examples 'a Berater', Berater::RateLimiter, 1, interval: :second
100
+ include_examples 'a Berater', Berater::StaticLimiter, 1
101
+ include_examples 'a Berater', Berater::Unlimiter, Float::INFINITY
134
102
  end
@@ -18,7 +18,7 @@ describe Berater::ConcurrencyLimiter do
18
18
  describe '#capacity' do
19
19
  def expect_capacity(capacity)
20
20
  limiter = described_class.new(:key, capacity)
21
- expect(limiter.capacity).to eq capacity
21
+ expect(limiter.capacity).to eq capacity.to_i
22
22
  end
23
23
 
24
24
  it { expect_capacity(0) }
@@ -36,22 +36,28 @@ describe Berater::ConcurrencyLimiter do
36
36
  it { expect_bad_capacity(-1) }
37
37
  it { expect_bad_capacity('1') }
38
38
  it { expect_bad_capacity(:one) }
39
+ it { expect_bad_capacity(Float::INFINITY) }
39
40
  end
40
41
  end
41
42
 
42
43
  describe '#timeout' do
43
44
  # see spec/utils_spec.rb
44
45
 
46
+ it 'defaults to nil' do
47
+ limiter = described_class.new(:key, 1)
48
+ expect(limiter.timeout).to be nil
49
+ end
50
+
45
51
  it 'saves the interval in original and millisecond format' do
46
52
  limiter = described_class.new(:key, 1, timeout: 3)
47
53
  expect(limiter.timeout).to be 3
48
- expect(limiter.instance_variable_get(:@timeout_msec)).to be (3 * 10**3)
54
+ expect(limiter.instance_variable_get(:@timeout)).to be (3 * 10**3)
49
55
  end
50
56
 
51
57
  it 'handles infinity' do
52
58
  limiter = described_class.new(:key, 1, timeout: Float::INFINITY)
53
59
  expect(limiter.timeout).to be Float::INFINITY
54
- expect(limiter.instance_variable_get(:@timeout_msec)).to be 0
60
+ expect(limiter.instance_variable_get(:@timeout)).to be 0
55
61
  end
56
62
  end
57
63
 
@@ -77,14 +83,14 @@ describe Berater::ConcurrencyLimiter do
77
83
  expect(limiter.limit).to be_a Berater::Lock
78
84
  expect(limiter.limit).to be_a Berater::Lock
79
85
 
80
- expect(limiter).to be_incapacitated
86
+ expect(limiter).to be_overloaded
81
87
  end
82
88
 
83
89
  context 'with capacity 0' do
84
90
  let(:limiter) { described_class.new(:key, 0) }
85
91
 
86
92
  it 'always fails' do
87
- expect(limiter).to be_incapacitated
93
+ expect(limiter).to be_overloaded
88
94
  end
89
95
  end
90
96
 
@@ -96,80 +102,80 @@ describe Berater::ConcurrencyLimiter do
96
102
 
97
103
  # since fractional cost is not supported
98
104
  expect(lock.capacity).to be 1
99
- expect(limiter).to be_incapacitated
105
+ expect(limiter).to be_overloaded
100
106
  end
101
107
  end
102
108
 
103
109
  it 'limit resets over time' do
104
110
  2.times { limiter.limit }
105
- expect(limiter).to be_incapacitated
111
+ expect(limiter).to be_overloaded
106
112
 
107
113
  Timecop.freeze(30)
108
114
 
109
115
  2.times { limiter.limit }
110
- expect(limiter).to be_incapacitated
116
+ expect(limiter).to be_overloaded
111
117
  end
112
118
 
113
119
  it 'limit resets with millisecond precision' do
114
120
  2.times { limiter.limit }
115
- expect(limiter).to be_incapacitated
121
+ expect(limiter).to be_overloaded
116
122
 
117
123
  # travel forward to just before first lock times out
118
124
  Timecop.freeze(29.999)
119
- expect(limiter).to be_incapacitated
125
+ expect(limiter).to be_overloaded
120
126
 
121
127
  # traveling one more millisecond will decrement the count
122
128
  Timecop.freeze(0.001)
123
129
  2.times { limiter.limit }
124
- expect(limiter).to be_incapacitated
130
+ expect(limiter).to be_overloaded
125
131
  end
126
132
 
127
133
  it 'accepts a dynamic capacity' do
128
- expect { limiter.limit(capacity: 0) }.to be_incapacitated
134
+ expect { limiter.limit(capacity: 0) }.to be_overloaded
129
135
  5.times { limiter.limit(capacity: 10) }
130
- expect { limiter }.to be_incapacitated
136
+ expect { limiter }.to be_overloaded
131
137
  end
132
138
 
133
139
  context 'with cost parameter' do
134
- it { expect { limiter.limit(cost: 4) }.to be_incapacitated }
140
+ it { expect { limiter.limit(cost: 4) }.to be_overloaded }
135
141
 
136
142
  it 'works within limit' do
137
143
  limiter.limit(cost: 2)
138
- expect(limiter).to be_incapacitated
144
+ expect(limiter).to be_overloaded
139
145
  end
140
146
 
141
147
  it 'releases full cost' do
142
148
  lock = limiter.limit(cost: 2)
143
- expect(limiter).to be_incapacitated
149
+ expect(limiter).to be_overloaded
144
150
 
145
151
  lock.release
146
- expect(limiter).not_to be_incapacitated
152
+ expect(limiter).not_to be_overloaded
147
153
 
148
154
  lock = limiter.limit(cost: 2)
149
- expect(limiter).to be_incapacitated
155
+ expect(limiter).to be_overloaded
150
156
  end
151
157
 
152
158
  it 'respects timeout' do
153
159
  limiter.limit(cost: 2)
154
- expect(limiter).to be_incapacitated
160
+ expect(limiter).to be_overloaded
155
161
 
156
162
  Timecop.freeze(30)
157
- expect(limiter).not_to be_incapacitated
163
+ expect(limiter).not_to be_overloaded
158
164
 
159
165
  limiter.limit(cost: 2)
160
- expect(limiter).to be_incapacitated
166
+ expect(limiter).to be_overloaded
161
167
  end
162
168
 
163
169
  context 'with fractional costs' do
164
170
  it 'rounds up' do
165
171
  limiter.limit(cost: 1.5)
166
- expect(limiter).to be_incapacitated
172
+ expect(limiter).to be_overloaded
167
173
  end
168
174
 
169
175
  it 'accumulates correctly' do
170
176
  limiter.limit(cost: 0.5) # => 1
171
177
  limiter.limit(cost: 0.7) # => 2
172
- expect(limiter).to be_incapacitated
178
+ expect(limiter).to be_overloaded
173
179
  end
174
180
  end
175
181
 
@@ -187,8 +193,8 @@ describe Berater::ConcurrencyLimiter do
187
193
  it 'works as expected' do
188
194
  expect(limiter_one.limit).to be_a Berater::Lock
189
195
 
190
- expect(limiter_one).to be_incapacitated
191
- expect(limiter_two).to be_incapacitated
196
+ expect(limiter_one).to be_overloaded
197
+ expect(limiter_two).to be_overloaded
192
198
  end
193
199
  end
194
200
 
@@ -202,22 +208,22 @@ describe Berater::ConcurrencyLimiter do
202
208
  one_lock = limiter_one.limit
203
209
  expect(one_lock).to be_a Berater::Lock
204
210
 
205
- expect(limiter_one).to be_incapacitated
206
- expect(limiter_two).not_to be_incapacitated
211
+ expect(limiter_one).to be_overloaded
212
+ expect(limiter_two).not_to be_overloaded
207
213
 
208
214
  two_lock = limiter_two.limit
209
215
  expect(two_lock).to be_a Berater::Lock
210
216
 
211
- expect(limiter_one).to be_incapacitated
212
- expect(limiter_two).to be_incapacitated
217
+ expect(limiter_one).to be_overloaded
218
+ expect(limiter_two).to be_overloaded
213
219
 
214
220
  one_lock.release
215
- expect(limiter_one).to be_incapacitated
216
- expect(limiter_two).not_to be_incapacitated
221
+ expect(limiter_one).to be_overloaded
222
+ expect(limiter_two).not_to be_overloaded
217
223
 
218
224
  two_lock.release
219
- expect(limiter_one).not_to be_incapacitated
220
- expect(limiter_two).not_to be_incapacitated
225
+ expect(limiter_one).not_to be_overloaded
226
+ expect(limiter_two).not_to be_overloaded
221
227
  end
222
228
  end
223
229
 
@@ -226,41 +232,41 @@ describe Berater::ConcurrencyLimiter do
226
232
  let(:limiter_two) { described_class.new(:two, 1) }
227
233
 
228
234
  it 'works as expected' do
229
- expect(limiter_one).not_to be_incapacitated
230
- expect(limiter_two).not_to be_incapacitated
235
+ expect(limiter_one).not_to be_overloaded
236
+ expect(limiter_two).not_to be_overloaded
231
237
 
232
238
  one_lock = limiter_one.limit
233
239
  expect(one_lock).to be_a Berater::Lock
234
240
 
235
- expect(limiter_one).to be_incapacitated
236
- expect(limiter_two).not_to be_incapacitated
241
+ expect(limiter_one).to be_overloaded
242
+ expect(limiter_two).not_to be_overloaded
237
243
 
238
244
  two_lock = limiter_two.limit
239
245
  expect(two_lock).to be_a Berater::Lock
240
246
 
241
- expect(limiter_one).to be_incapacitated
242
- expect(limiter_two).to be_incapacitated
247
+ expect(limiter_one).to be_overloaded
248
+ expect(limiter_two).to be_overloaded
243
249
  end
244
250
  end
245
251
  end
246
252
 
247
- describe '#overloaded?' do
248
- let(:limiter) { described_class.new(:key, 1, timeout: 30) }
253
+ describe '#utilization' do
254
+ let(:limiter) { described_class.new(:key, 10, timeout: 30) }
249
255
 
250
256
  it 'works' do
251
- expect(limiter.overloaded?).to be false
252
- lock = limiter.limit
253
- expect(limiter.overloaded?).to be true
254
- lock.release
255
- expect(limiter.overloaded?).to be false
256
- end
257
+ expect(limiter.utilization).to be 0.0
257
258
 
258
- it 'respects timeout' do
259
- expect(limiter.overloaded?).to be false
260
- lock = limiter.limit
261
- expect(limiter.overloaded?).to be true
262
- Timecop.freeze(30)
263
- expect(limiter.overloaded?).to be false
259
+ 2.times { limiter.limit }
260
+ expect(limiter.utilization).to be 0.2
261
+
262
+ Timecop.freeze(15)
263
+
264
+ 8.times { limiter.limit }
265
+ expect(limiter.utilization).to be 1.0
266
+
267
+ Timecop.freeze(15)
268
+
269
+ expect(limiter.utilization).to be 0.8
264
270
  end
265
271
  end
266
272