berater 0.6.0 → 0.8.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,83 +1,61 @@
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
11
7
 
12
8
  def matches?(obj)
13
- begin
14
- case obj
15
- when Proc
16
- # eg. expect { ... }.to be_overrated
17
- res = obj.call
18
-
19
- if res.is_a? Berater::Limiter
20
- # eg. expect { Berater.new(...) }.to be_overloaded
21
- @limiter = res
22
- res.overloaded?
23
- else
24
- # eg. expect { Berater(...) }.to be_overloaded
25
- # eg. expect { limiter.limit }.to be_overloaded
26
- false
27
- end
28
- when Berater::Limiter
29
- # eg. expect(Berater.new(...)).to be_overloaded
30
- @limiter = obj
31
- obj.overloaded?
9
+ case obj
10
+ when Proc
11
+ # eg. expect { ... }.to be_overloaded
12
+ res = obj.call
13
+
14
+ if res.is_a? Berater::Limiter
15
+ # eg. expect { Berater.new(...) }.to be_overloaded
16
+ @limiter = res
17
+ @limiter.utilization >= 1
18
+ else
19
+ # eg. expect { Berater(...) }.to be_overloaded
20
+ # eg. expect { limiter.limit }.to be_overloaded
21
+ false
32
22
  end
33
- rescue @type
34
- true
23
+ when Berater::Limiter
24
+ # eg. expect(Berater.new(...)).to be_overloaded
25
+ @limiter = obj
26
+ @limiter.utilization >= 1
35
27
  end
28
+ rescue Berater::Overloaded
29
+ true
36
30
  end
37
31
 
38
32
  def description
39
33
  if @limiter
40
- "be #{verb}"
34
+ "be overloaded"
41
35
  else
42
- "raise #{@type}"
36
+ "raise #{Berater::Overloaded}"
43
37
  end
44
38
  end
45
39
 
46
40
  def failure_message
47
41
  if @limiter
48
- "expected to be #{verb}"
42
+ "expected to be overloaded"
49
43
  else
50
- "expected #{@type} to be raised"
44
+ "expected #{Berater::Overloaded} to be raised"
51
45
  end
52
46
  end
53
47
 
54
48
  def failure_message_when_negated
55
49
  if @limiter
56
- "expected not to be #{verb}"
50
+ "expected not to be overloaded"
57
51
  else
58
- "did not expect #{@type} to be raised"
52
+ "did not expect #{Berater::Overloaded} to be raised"
59
53
  end
60
54
  end
61
-
62
- private def verb
63
- @type.to_s.split('::')[-1].downcase
64
- end
65
55
  end
66
56
 
67
57
  def be_overloaded
68
- Overloaded.new(Berater::Overloaded)
69
- end
70
-
71
- def be_overrated
72
- Overloaded.new(Berater::RateLimiter::Overrated)
73
- end
74
-
75
- def be_incapacitated
76
- Overloaded.new(Berater::ConcurrencyLimiter::Incapacitated)
77
- end
78
-
79
- def be_inhibited
80
- Overloaded.new(Berater::Inhibitor::Inhibited)
58
+ Overloaded.new
81
59
  end
82
60
  end
83
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,52 +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
7
 
8
- def test_mode=(mode)
9
- unless [ nil, :pass, :fail ].include?(mode)
10
- raise ArgumentError, "invalid mode: #{Berater.test_mode}"
11
- end
8
+ def test_mode=(mode)
9
+ unless [ nil, :pass, :fail ].include?(mode)
10
+ raise ArgumentError, "invalid mode: #{Berater.test_mode}"
11
+ end
12
12
 
13
- @test_mode = mode
13
+ @test_mode = mode
14
+ end
14
15
 
15
- # overload class methods
16
- unless Berater::Limiter.singleton_class.ancestors.include?(TestMode)
17
- Berater::Limiter.singleton_class.prepend(TestMode)
16
+ def reset
17
+ super
18
+ @test_mode = nil
18
19
  end
19
20
  end
20
21
 
21
- module TestMode
22
- def new(*args, **opts)
23
- return super unless Berater.test_mode
24
-
25
- # chose a stub class with desired behavior
26
- stub_klass = case Berater.test_mode
27
- when :pass
28
- Berater::Unlimiter
29
- when :fail
30
- Berater::Inhibitor
31
- end
32
-
33
- # don't stub self
34
- return super if self < stub_klass
35
-
36
- # swap out limit and overloaded? methods with stub
37
- super.tap do |instance|
38
- stub = stub_klass.allocate
39
- stub.send(:initialize, *args, **opts)
40
-
41
- instance.define_singleton_method(:limit) do |**opts, &block|
42
- stub.limit(**opts, &block)
43
- end
44
-
45
- instance.define_singleton_method(:overloaded?) do
46
- stub.overloaded?
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
47
32
  end
48
33
  end
49
34
  end
50
35
  end
51
36
 
52
37
  end
38
+
39
+ # prepend class methods
40
+ Berater.singleton_class.prepend Berater::TestMode
41
+
42
+ # stub each Limiter subclass
43
+ ObjectSpace.each_object(Class).each do |klass|
44
+ next unless klass < Berater::Limiter
45
+
46
+ klass.prepend Berater::Limiter::TestMode
47
+ end
@@ -5,17 +5,19 @@ module Berater
5
5
  super(key, Float::INFINITY, **opts)
6
6
  end
7
7
 
8
- def limit(**opts, &block)
9
- yield_lock(Lock.new(Float::INFINITY, 0), &block)
8
+ def to_s
9
+ "#<#{self.class}>"
10
10
  end
11
11
 
12
- def overloaded?
13
- false
14
- end
12
+ protected
15
13
 
16
- protected def capacity=(*)
14
+ def capacity=(*)
17
15
  @capacity = Float::INFINITY
18
16
  end
19
17
 
18
+ def acquire_lock(*)
19
+ Lock.new(Float::INFINITY, 0)
20
+ end
21
+
20
22
  end
21
23
  end
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.0'
2
+ VERSION = '0.8.0'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -24,50 +24,15 @@ 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
+ shared_examples 'a Berater' do |klass, capacity, **opts|
28
+ describe '.new' do
29
+ let(:limiter) { Berater.new(:key, capacity, **opts) }
30
30
 
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
39
-
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
45
- end
46
-
47
- context 'inhibited mode' do
48
- let(:limiter) { Berater.new(:key, 0) }
49
-
50
- it 'instantiates an Inhibitor' do
51
- expect(limiter).to be_a Berater::Inhibitor
52
- expect(limiter.key).to be :key
53
- end
54
-
55
- it 'inherits redis' do
56
- expect(limiter.redis).to be Berater.redis
57
- end
58
-
59
- it 'accepts options' do
60
- redis = double('Redis')
61
- limiter = Berater.new(:key, 0, redis: redis)
62
- expect(limiter.redis).to be redis
31
+ it 'instantiates the right class' do
32
+ expect(limiter).to be_a klass
63
33
  end
64
- end
65
34
 
66
- context 'rate mode' do
67
- let(:limiter) { Berater.new(:key, 1, :second) }
68
-
69
- it 'instantiates a RateLimiter' do
70
- expect(limiter).to be_a Berater::RateLimiter
35
+ it 'sets the key' do
71
36
  expect(limiter.key).to be :key
72
37
  end
73
38
 
@@ -75,60 +40,45 @@ describe Berater do
75
40
  expect(limiter.redis).to be Berater.redis
76
41
  end
77
42
 
78
- it 'accepts options' do
43
+ it 'accepts an optional redis parameter' do
79
44
  redis = double('Redis')
80
- limiter = Berater.new(:key, 1, :second, redis: redis)
45
+ limiter = Berater.new(:key, capacity, opts.merge(redis: redis))
81
46
  expect(limiter.redis).to be redis
82
47
  end
83
48
  end
84
49
 
85
- context 'concurrency mode' do
86
- let(:limiter) { Berater.new(:key, 1) }
50
+ describe 'Berater() convenience method' do
51
+ let(:limiter) { Berater(:key, capacity, **opts) }
87
52
 
88
- it 'instantiates a ConcurrencyLimiter' do
89
- expect(limiter).to be_a Berater::ConcurrencyLimiter
90
- expect(limiter.key).to be :key
53
+ it 'creates a limiter' do
54
+ expect(limiter).to be_a klass
91
55
  end
92
56
 
93
- it 'inherits redis' do
94
- expect(limiter.redis).to be Berater.redis
57
+ it 'creates an equivalent limiter' do
58
+ expect(limiter).to eq Berater.new(:key, capacity, **opts)
95
59
  end
96
60
 
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
61
+ context 'with a block' do
62
+ before { Berater.test_mode = :pass }
104
63
 
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
64
+ subject { Berater(:key, capacity, **opts) { 123 } }
111
65
 
112
- context 'with a block' do
113
66
  it 'creates a limiter and calls limit' do
114
- limiter = Berater(:key, *args)
115
67
  expect(klass).to receive(:new).and_return(limiter)
116
68
  expect(limiter).to receive(:limit).and_call_original
69
+ subject
70
+ end
117
71
 
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
72
+ it 'yields' do
73
+ is_expected.to be 123
124
74
  end
125
75
  end
126
76
  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
77
  end
133
78
 
79
+ include_examples 'a Berater', Berater::ConcurrencyLimiter, 1, timeout: 1
80
+ include_examples 'a Berater', Berater::Inhibitor, 0
81
+ include_examples 'a Berater', Berater::RateLimiter, 1, interval: :second
82
+ include_examples 'a Berater', Berater::StaticLimiter, 1
83
+ include_examples 'a Berater', Berater::Unlimiter, Float::INFINITY
134
84
  end