berater 0.7.1 → 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.
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