berater 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c12ee8f042d6ebb193f114cd093585c14a4a28b802d7a90f79ba5ce41bbd82e6
4
- data.tar.gz: d12ebb1107ea0b24208bfb3ff663a98097cd3367c3972418a139cae9b501e4a1
3
+ metadata.gz: c32c1de752ccc7a2a71e4628b99f3561db623374dabaf95af39aadcc3b1a2d16
4
+ data.tar.gz: 33019954de1d79522ed7105e9f0a7da3a204df74cacd25aa4b57e749a81047bb
5
5
  SHA512:
6
- metadata.gz: 491cd616903af917866a41f8b2a4bcc79a469a879f57f1872ce8f4a42c00f1309979c04c142c0083246791aef6cd3847c21197dee0b611ff326ca833cc44ae52
7
- data.tar.gz: 7ba3fd90f8b439e4ad2fe1d5a8008c69978a90c206bc6694e9f470796019d2ac849dddaa85039b9c730e3e20edfec3411cc1275598866c63207c209e688f2a3a
6
+ metadata.gz: 89460a9a81a8d384addb095c425da0eed4fdbe0e15ecd85ee97ffcf4292441deeb34c7e4cd4591adca06096a17f7c131b79c6474d9efb2b635fbe398ad68d00e
7
+ data.tar.gz: 0a331b939e4c19817c42a8aa8fc5be7fd59d63f6becd5fe6ed1c226690d1524ef040a28d8bcb625d75452ab2f5c8539ddf69093bbd30c4ba9e3353259a3e568d
data/lib/berater.rb CHANGED
@@ -31,8 +31,10 @@ module Berater
31
31
  if opts[:interval]
32
32
  args << opts.delete(:interval)
33
33
  Berater::RateLimiter
34
- else
34
+ elsif opts[:timeout]
35
35
  Berater::ConcurrencyLimiter
36
+ else
37
+ Berater::StaticLimiter
36
38
  end
37
39
  end.yield_self do |klass|
38
40
  args = [ key, capacity, *args ].compact
@@ -49,17 +51,13 @@ module Berater
49
51
  end
50
52
 
51
53
  # convenience method
52
- def Berater(key, capacity, **opts, &block)
53
- limiter = Berater.new(key, capacity, **opts)
54
- if block_given?
55
- limiter.limit(&block)
56
- else
57
- limiter
58
- end
54
+ def Berater(*args, **opts, &block)
55
+ Berater::Utils.convenience_fn(Berater, *args, **opts, &block)
59
56
  end
60
57
 
61
58
  # load limiters
62
59
  require 'berater/concurrency_limiter'
63
60
  require 'berater/inhibitor'
64
61
  require 'berater/rate_limiter'
62
+ require 'berater/static_limiter'
65
63
  require 'berater/unlimiter'
@@ -1,21 +1,22 @@
1
1
  module Berater
2
2
  class ConcurrencyLimiter < Limiter
3
3
 
4
- attr_reader :timeout
5
-
6
4
  def initialize(key, capacity, **opts)
7
5
  super(key, capacity, **opts)
8
6
 
9
- # round fractional capacity
7
+ # truncate fractional capacity
10
8
  self.capacity = capacity.to_i
11
9
 
12
10
  self.timeout = opts[:timeout] || 0
13
11
  end
14
12
 
13
+ def timeout
14
+ options[:timeout]
15
+ end
16
+
15
17
  private def timeout=(timeout)
16
- @timeout = timeout
17
18
  timeout = 0 if timeout == Float::INFINITY
18
- @timeout_msec = Berater::Utils.to_msec(timeout)
19
+ @timeout = Berater::Utils.to_msec(timeout)
19
20
  end
20
21
 
21
22
  LUA_SCRIPT = Berater::LuaScript(<<~LUA
@@ -72,24 +73,19 @@ module Berater
72
73
 
73
74
  count, *lock_ids = LUA_SCRIPT.eval(
74
75
  redis,
75
- [ cache_key(key), cache_key('lock_id') ],
76
- [ capacity, ts, @timeout_msec, cost ]
76
+ [ cache_key, self.class.cache_key('lock_id') ],
77
+ [ capacity, ts, @timeout, cost ]
77
78
  )
78
79
 
79
80
  raise Overloaded if lock_ids.empty?
80
81
 
81
82
  release_fn = if cost > 0
82
- proc { release(lock_ids) }
83
+ proc { redis.zrem(cache_key, lock_ids) }
83
84
  end
84
85
 
85
86
  Lock.new(capacity, count, release_fn)
86
87
  end
87
88
 
88
- private def release(lock_ids)
89
- res = redis.zrem(cache_key(key), lock_ids)
90
- res == true || res == lock_ids.count # depending on which version of Redis
91
- end
92
-
93
89
  def to_s
94
90
  "#<#{self.class}(#{key}: #{capacity} at a time)>"
95
91
  end
@@ -5,6 +5,10 @@ module Berater
5
5
  super(key, 0, **opts)
6
6
  end
7
7
 
8
+ def to_s
9
+ "#<#{self.class}>"
10
+ end
11
+
8
12
  protected def acquire_lock(*)
9
13
  raise Overloaded
10
14
  end
@@ -43,10 +43,6 @@ module Berater
43
43
  1.0
44
44
  end
45
45
 
46
- def to_s
47
- "#<#{self.class}>"
48
- end
49
-
50
46
  def ==(other)
51
47
  self.class == other.class &&
52
48
  self.key == other.key &&
@@ -92,8 +88,22 @@ module Berater
92
88
  raise NotImplementedError
93
89
  end
94
90
 
95
- def cache_key(key)
96
- "#{self.class}:#{key}"
91
+ def cache_key(subkey = nil)
92
+ instance_key = subkey.nil? ? key : "#{key}:#{subkey}"
93
+ self.class.cache_key(instance_key)
94
+ end
95
+
96
+ def self.cache_key(key)
97
+ klass = to_s.split(':')[-1]
98
+ "Berater:#{klass}:#{key}"
99
+ end
100
+
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)
106
+ end
97
107
  end
98
108
 
99
109
  end
data/lib/berater/lock.rb CHANGED
@@ -19,7 +19,8 @@ module Berater
19
19
  raise 'lock already released' unless locked?
20
20
 
21
21
  @released_at = Time.now
22
- @release_fn ? @release_fn.call : true
22
+ @release_fn&.call
23
+ nil
23
24
  end
24
25
 
25
26
  end
@@ -1,18 +1,19 @@
1
1
  module Berater
2
2
  class RateLimiter < Limiter
3
3
 
4
- attr_accessor :interval
5
-
6
4
  def initialize(key, capacity, interval, **opts)
5
+ super(key, capacity, interval, **opts)
7
6
  self.interval = interval
8
- super(key, capacity, @interval_msec, **opts)
7
+ end
8
+
9
+ def interval
10
+ args[0]
9
11
  end
10
12
 
11
13
  private def interval=(interval)
12
- @interval = interval
13
- @interval_msec = Berater::Utils.to_msec(interval)
14
+ @interval = Berater::Utils.to_msec(interval)
14
15
 
15
- unless @interval_msec > 0
16
+ unless @interval > 0
16
17
  raise ArgumentError, 'interval must be > 0'
17
18
  end
18
19
  end
@@ -76,8 +77,8 @@ module Berater
76
77
 
77
78
  count, allowed = LUA_SCRIPT.eval(
78
79
  redis,
79
- [ cache_key(key) ],
80
- [ ts, capacity, @interval_msec, cost ]
80
+ [ cache_key ],
81
+ [ ts, capacity, @interval, cost ]
81
82
  )
82
83
 
83
84
  count = count.include?('.') ? count.to_f : count.to_i
data/lib/berater/rspec.rb CHANGED
@@ -8,7 +8,6 @@ RSpec.configure do |config|
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
@@ -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,36 +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
- raise Overloaded
23
- else
24
- 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
25
33
  end
26
34
  end
27
35
  end
28
36
 
29
37
  end
30
38
 
39
+ # prepend class methods
40
+ Berater.singleton_class.prepend Berater::TestMode
41
+
31
42
  # stub each Limiter subclass
32
43
  ObjectSpace.each_object(Class).each do |klass|
33
44
  next unless klass < Berater::Limiter
34
45
 
35
- klass.prepend(Berater::TestMode)
46
+ klass.prepend Berater::Limiter::TestMode
36
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.7.1'
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 'Unlimiter 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 'Inhibitor 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, interval: :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, interval: :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, capacity, **opts|
107
- it 'creates a limiter' do
108
- limiter = Berater(:key, capacity, **opts)
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, capacity, **opts)
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, capacity, **opts) { 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, interval: :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
@@ -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
 
@@ -1,7 +1,9 @@
1
1
  describe Berater::Inhibitor do
2
+ subject { described_class.new }
3
+
2
4
  describe '.new' do
3
5
  it 'initializes without any arguments or options' do
4
- expect(described_class.new).to be_a described_class
6
+ is_expected.to be_a described_class
5
7
  end
6
8
 
7
9
  it 'initializes with any arguments and options' do
@@ -9,15 +11,18 @@ describe Berater::Inhibitor do
9
11
  end
10
12
 
11
13
  it 'has default values' do
12
- expect(described_class.new.key).to be :inhibitor
13
- expect(described_class.new.redis).to be Berater.redis
14
+ expect(subject.key).to be :inhibitor
15
+ expect(subject.redis).to be Berater.redis
14
16
  end
15
17
  end
16
18
 
17
19
  describe '#limit' do
18
- subject { described_class.new }
19
-
20
20
  it_behaves_like 'it is overloaded'
21
21
  end
22
22
 
23
+ describe '#to_s' do
24
+ it do
25
+ expect(subject.to_s).to include described_class.to_s
26
+ end
27
+ end
23
28
  end
data/spec/limiter_spec.rb CHANGED
@@ -44,6 +44,10 @@ describe Berater::Limiter do
44
44
  expect {
45
45
  subject.limit(cost: -1)
46
46
  }.to raise_error(ArgumentError)
47
+
48
+ expect {
49
+ subject.limit(cost: Float::INFINITY)
50
+ }.to raise_error(ArgumentError)
47
51
  end
48
52
  end
49
53
  end
@@ -61,12 +65,6 @@ describe Berater::Limiter do
61
65
  )
62
66
  end
63
67
 
64
- it 'equals something with equvalent initialization parameters' do
65
- expect(limiter).to eq(
66
- Berater::RateLimiter.new(:key, 1, 1)
67
- )
68
- end
69
-
70
68
  it 'does not equal something different' do
71
69
  expect(limiter).not_to eq(
72
70
  Berater::RateLimiter.new(:key, 2, :second)
@@ -104,4 +102,56 @@ describe Berater::Limiter do
104
102
  end
105
103
  end
106
104
 
105
+ describe '#cache_key' do
106
+ subject { klass.new(:key).send(:cache_key) }
107
+
108
+ context 'with Unlimiter' do
109
+ let(:klass) { Berater::Unlimiter }
110
+
111
+ it do
112
+ is_expected.to eq 'Berater:Unlimiter:key'
113
+ end
114
+ end
115
+
116
+ context 'with custom limiter' do
117
+ MyLimiter = Class.new(Berater::Unlimiter)
118
+
119
+ let(:klass) { MyLimiter }
120
+
121
+ it 'adds Berater prefix' do
122
+ is_expected.to eq 'Berater:MyLimiter:key'
123
+ end
124
+ end
125
+ end
126
+
127
+ describe '.cache_key' do
128
+ subject { klass.send(:cache_key, :key) }
129
+
130
+ context 'with Unlimiter' do
131
+ let(:klass) { Berater::Unlimiter }
132
+
133
+ it do
134
+ is_expected.to eq 'Berater:Unlimiter:key'
135
+ end
136
+ end
137
+
138
+ context 'with custom limiter' do
139
+ MyLimiter = Class.new(Berater::Unlimiter)
140
+
141
+ let(:klass) { MyLimiter }
142
+
143
+ it 'adds Berater prefix' do
144
+ is_expected.to eq 'Berater:MyLimiter:key'
145
+ end
146
+ end
147
+ end
148
+
149
+ describe '.inherited' do
150
+ it 'creates convenience methods' do
151
+ expect(Berater.method(:Unlimiter)).to be_a Method
152
+ expect(Berater::Unlimiter()).to be_a Berater::Unlimiter
153
+ expect {|b| Berater::Unlimiter(&b) }.to yield_control
154
+ end
155
+ end
156
+
107
157
  end
@@ -37,6 +37,7 @@ describe Berater::RateLimiter do
37
37
  it { expect_bad_capacity(-1) }
38
38
  it { expect_bad_capacity('1') }
39
39
  it { expect_bad_capacity(:one) }
40
+ it { expect_bad_capacity(Float::INFINITY) }
40
41
  end
41
42
  end
42
43
 
@@ -47,7 +48,7 @@ describe Berater::RateLimiter do
47
48
 
48
49
  it 'saves the interval in original and millisecond format' do
49
50
  expect(subject.interval).to be :second
50
- expect(subject.instance_variable_get(:@interval_msec)).to be 10**3
51
+ expect(subject.instance_variable_get(:@interval)).to be 10**3
51
52
  end
52
53
 
53
54
  it 'must be > 0' do
data/spec/riddle_spec.rb CHANGED
@@ -47,7 +47,7 @@ class ConcurrenyRiddler
47
47
  ensure
48
48
  # decrement counter
49
49
  if lock
50
- key = limiter.send(:cache_key, :key)
50
+ key = limiter.send(:cache_key)
51
51
  count, ts = limiter.redis.get(key).split ';'
52
52
  limiter.redis.set(key, "#{count.to_f - 1};#{ts}")
53
53
  end
@@ -0,0 +1,79 @@
1
+ describe Berater::StaticLimiter do
2
+ it_behaves_like 'a limiter', described_class.new(:key, 3)
3
+ it_behaves_like 'a limiter', described_class.new(:key, 3.5)
4
+
5
+ describe '#limit' do
6
+ let(:limiter) { described_class.new(:key, 3) }
7
+
8
+ it 'limits excessive calls' do
9
+ 3.times { limiter.limit }
10
+
11
+ expect(limiter).to be_overloaded
12
+ end
13
+
14
+ context 'when capacity is a Float' do
15
+ let(:limiter) { described_class.new(:key, 1.5) }
16
+
17
+ it 'still works' do
18
+ limiter.limit
19
+ expect(limiter).not_to be_overloaded
20
+
21
+ expect { limiter.limit }.to be_overloaded
22
+
23
+ limiter.limit(cost: 0.5)
24
+ end
25
+ end
26
+
27
+ it 'accepts a dynamic capacity' do
28
+ limiter = described_class.new(:key, 1)
29
+
30
+ expect { limiter.limit(capacity: 0) }.to be_overloaded
31
+ 5.times { limiter.limit(capacity: 10) }
32
+ expect { limiter }.to be_overloaded
33
+ end
34
+
35
+ context 'works with cost parameter' do
36
+ let(:limiter) { described_class.new(:key, 3) }
37
+
38
+ it { expect { limiter.limit(cost: 4) }.to be_overloaded }
39
+
40
+ it 'works within limit' do
41
+ limiter.limit(cost: 3)
42
+ expect { limiter.limit }.to be_overloaded
43
+ end
44
+
45
+ context 'when cost is a Float' do
46
+ it 'still works' do
47
+ 2.times { limiter.limit(cost: 1.5) }
48
+ expect(limiter).to be_overloaded
49
+ end
50
+
51
+ it 'calculates contention correctly' do
52
+ lock = limiter.limit(cost: 1.5)
53
+ expect(lock.contention).to be 1.5
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ describe '#utilization' do
60
+ let(:limiter) { described_class.new(:key, 10) }
61
+
62
+ it do
63
+ expect(limiter.utilization).to be 0.0
64
+
65
+ 2.times { limiter.limit }
66
+ expect(limiter.utilization).to be 0.2
67
+
68
+ 8.times { limiter.limit }
69
+ expect(limiter.utilization).to be 1.0
70
+ end
71
+ end
72
+
73
+ describe '#to_s' do
74
+ let(:limiter) { described_class.new(:key, 3) }
75
+
76
+ it { expect(limiter.to_s).to include '3' }
77
+ end
78
+
79
+ end
@@ -1,8 +1,4 @@
1
1
  describe Berater::TestMode do
2
- after do
3
- Berater.test_mode = nil
4
- end
5
-
6
2
  context 'after test_mode.rb has been loaded' do
7
3
  it 'monkey patches Berater' do
8
4
  expect(Berater).to respond_to(:test_mode)
@@ -13,8 +9,8 @@ describe Berater::TestMode do
13
9
  end
14
10
 
15
11
  it 'prepends Limiter subclasses' do
16
- expect(Berater::Unlimiter.ancestors).to include(described_class)
17
- expect(Berater::Inhibitor.ancestors).to include(described_class)
12
+ expect(Berater::Unlimiter.ancestors).to include(Berater::Limiter::TestMode)
13
+ expect(Berater::Inhibitor.ancestors).to include(Berater::Limiter::TestMode)
18
14
  end
19
15
 
20
16
  it 'preserves the original functionality via super' do
@@ -55,6 +51,16 @@ describe Berater::TestMode do
55
51
  end
56
52
  end
57
53
 
54
+ describe '.reset' do
55
+ before { Berater.test_mode = :pass }
56
+
57
+ it 'resets test_mode' do
58
+ expect(Berater.test_mode).to be :pass
59
+ Berater.reset
60
+ expect(Berater.test_mode).to be nil
61
+ end
62
+ end
63
+
58
64
  shared_examples 'it supports test_mode' do
59
65
  before do
60
66
  # without hitting Redis
@@ -1,9 +1,11 @@
1
1
  describe Berater::Unlimiter do
2
+ subject { described_class.new }
3
+
2
4
  it_behaves_like 'a limiter', described_class.new
3
5
 
4
6
  describe '.new' do
5
7
  it 'initializes without any arguments or options' do
6
- expect(described_class.new).to be_a described_class
8
+ is_expected.to be_a described_class
7
9
  end
8
10
 
9
11
  it 'initializes with any arguments and options' do
@@ -11,14 +13,12 @@ describe Berater::Unlimiter do
11
13
  end
12
14
 
13
15
  it 'has default values' do
14
- expect(described_class.new.key).to be :unlimiter
15
- expect(described_class.new.redis).to be Berater.redis
16
+ expect(subject.key).to be :unlimiter
17
+ expect(subject.redis).to be Berater.redis
16
18
  end
17
19
  end
18
20
 
19
21
  describe '#limit' do
20
- subject { described_class.new }
21
-
22
22
  it_behaves_like 'it is not overloaded'
23
23
 
24
24
  it 'is never overloaded' do
@@ -27,4 +27,10 @@ describe Berater::Unlimiter do
27
27
  end
28
28
  end
29
29
  end
30
+
31
+ describe '#to_s' do
32
+ it do
33
+ expect(subject.to_s).to include described_class.to_s
34
+ end
35
+ end
30
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: berater
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.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-15 00:00:00.000000000 Z
11
+ date: 2021-03-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -138,6 +138,7 @@ files:
138
138
  - lib/berater/rate_limiter.rb
139
139
  - lib/berater/rspec.rb
140
140
  - lib/berater/rspec/matchers.rb
141
+ - lib/berater/static_limiter.rb
141
142
  - lib/berater/test_mode.rb
142
143
  - lib/berater/unlimiter.rb
143
144
  - lib/berater/utils.rb
@@ -152,6 +153,7 @@ files:
152
153
  - spec/matchers_spec.rb
153
154
  - spec/rate_limiter_spec.rb
154
155
  - spec/riddle_spec.rb
156
+ - spec/static_limiter_spec.rb
155
157
  - spec/test_mode_spec.rb
156
158
  - spec/unlimiter_spec.rb
157
159
  - spec/utils_spec.rb
@@ -190,5 +192,6 @@ test_files:
190
192
  - spec/utils_spec.rb
191
193
  - spec/berater_spec.rb
192
194
  - spec/limiter_spec.rb
195
+ - spec/static_limiter_spec.rb
193
196
  - spec/inhibitor_spec.rb
194
197
  - spec/unlimiter_spec.rb