berater 0.6.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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