berater 0.6.0 → 0.8.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,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