berater 0.2.0 → 0.3.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: dec8c5d6d428f795d1489dace166451e2a3a42592d00d71155ed5cd0d5aaa909
4
- data.tar.gz: 6956b6c2804b6d616074e239439d4f2a8e551e900db7801226948b9eb764d0fc
3
+ metadata.gz: d007dbb664fa57c921047a28dd1e5a591cf5c51fba4931dbc367ab552e4b8a8d
4
+ data.tar.gz: c710d31a2d8cfdc50af4c2fae87cabad177096d13da52a7f9c48b61aa31b6038
5
5
  SHA512:
6
- metadata.gz: 327cd2f3a7430c2b81bd1988abd636bfc7d97048c762d3807555e9c1196153570dbe3045fae813852a907ec00b89b8d2c36e9269ed0562419c8d98dd6c1d2b26
7
- data.tar.gz: ea1cb39e9201091e4a1357e888ac36f630c326a271e3cfcafa13298b8f54424f776f4dd488e046e2178905d0df9fb5c3b1f0d454e331823cafe5dd338fe9ea6c
6
+ metadata.gz: 0d9c211bb65e7341b98e637d3f0ce0e31f0a879437c1c309fb01e1efab5e5c6b2d5044158931ab16a67cbbdebd00ddb5f10afc97430230b153d8cc236f7517ab
7
+ data.tar.gz: 5d8b38bfd7683df641fd6891cd1a493079453ea6c61b2880581eeb2d963fb171365fb3448443e2548b34297d8dcfa352e67cff5688cd24d25422efb5e55d4afd
data/lib/berater.rb CHANGED
@@ -15,7 +15,23 @@ module Berater
15
15
  yield self
16
16
  end
17
17
 
18
- def new(key, mode, *args, **opts)
18
+ def new(key, mode = nil, *args, **opts, &block)
19
+ if mode.nil?
20
+ unless args.empty?
21
+ raise ArgumentError, '0 arguments expected with block'
22
+ end
23
+
24
+ unless block_given?
25
+ raise ArgumentError, 'expected either mode or block'
26
+ end
27
+
28
+ mode, *args = DSL.eval(&block)
29
+ else
30
+ if block_given?
31
+ raise ArgumentError, 'expected either mode or block, not both'
32
+ end
33
+ end
34
+
19
35
  klass = MODES[mode.to_sym]
20
36
 
21
37
  unless klass
@@ -42,8 +58,8 @@ def Berater(key, mode, *args, **opts, &block)
42
58
  Berater.new(key, mode, *args, **opts).limit(&block)
43
59
  end
44
60
 
45
- # load and register limiters
46
- require 'berater/base_limiter'
61
+ # load limiters
62
+ require 'berater/limiter'
47
63
  require 'berater/concurrency_limiter'
48
64
  require 'berater/inhibitor'
49
65
  require 'berater/rate_limiter'
@@ -53,3 +69,5 @@ Berater.register(:concurrency, Berater::ConcurrencyLimiter)
53
69
  Berater.register(:inhibited, Berater::Inhibitor)
54
70
  Berater.register(:rate, Berater::RateLimiter)
55
71
  Berater.register(:unlimited, Berater::Unlimiter)
72
+
73
+ require 'berater/dsl'
@@ -1,5 +1,5 @@
1
1
  module Berater
2
- class ConcurrencyLimiter < BaseLimiter
2
+ class ConcurrencyLimiter < Limiter
3
3
 
4
4
  class Incapacitated < Overloaded; end
5
5
 
@@ -85,5 +85,9 @@ module Berater
85
85
  res == true || res == 1 # depending on which version of Redis
86
86
  end
87
87
 
88
+ def to_s
89
+ "#<#{self.class}(#{key}: #{capacity} at a time)>"
90
+ end
91
+
88
92
  end
89
93
  end
@@ -0,0 +1,57 @@
1
+ module Berater
2
+ module DSL
3
+ extend self
4
+
5
+ def eval &block
6
+ @keywords ||= Class.new do
7
+ # create a class where DSL keywords are methods
8
+ KEYWORDS.each do |keyword|
9
+ define_singleton_method(keyword) { keyword }
10
+ end
11
+ end
12
+
13
+ install
14
+ @keywords.class_eval &block
15
+ ensure
16
+ uninstall
17
+ end
18
+
19
+ private
20
+
21
+ def each &block
22
+ Berater::MODES.map do |mode, limiter|
23
+ next unless limiter.const_defined?(:DSL, false)
24
+ limiter.const_get(:DSL)
25
+ end.compact.each(&block)
26
+ end
27
+
28
+ KEYWORDS = [
29
+ :second, :minute, :hour,
30
+ :unlimited, :inhibited,
31
+ ].freeze
32
+
33
+ def install
34
+ Integer.class_eval do
35
+ def per(unit)
36
+ [ :rate, self, unit ]
37
+ end
38
+ alias every per
39
+
40
+ def at_once
41
+ [ :concurrency, self ]
42
+ end
43
+ alias concurrently at_once
44
+ alias at_a_time at_once
45
+ end
46
+ end
47
+
48
+ def uninstall
49
+ Integer.remove_method :per
50
+ Integer.remove_method :every
51
+
52
+ Integer.remove_method :at_once
53
+ Integer.remove_method :concurrently
54
+ Integer.remove_method :at_a_time
55
+ end
56
+ end
57
+ end
@@ -1,5 +1,5 @@
1
1
  module Berater
2
- class Inhibitor < BaseLimiter
2
+ class Inhibitor < Limiter
3
3
 
4
4
  class Inhibited < Overloaded; end
5
5
 
@@ -1,5 +1,5 @@
1
1
  module Berater
2
- class BaseLimiter
2
+ class Limiter
3
3
 
4
4
  attr_reader :key, :options
5
5
 
@@ -11,6 +11,10 @@ module Berater
11
11
  raise NotImplementedError
12
12
  end
13
13
 
14
+ def to_s
15
+ "#<#{self.class}>"
16
+ end
17
+
14
18
  protected
15
19
 
16
20
  def initialize(key, **opts)
data/lib/berater/lock.rb CHANGED
@@ -17,7 +17,7 @@ module Berater
17
17
  end
18
18
 
19
19
  def expired?
20
- timeout > 0 && @locked_at + timeout < Time.now
20
+ timeout ? @locked_at + timeout < Time.now : false
21
21
  end
22
22
 
23
23
  def release
@@ -28,8 +28,8 @@ module Berater
28
28
  @release_fn ? @release_fn.call : true
29
29
  end
30
30
 
31
- private def timeout
32
- limiter.respond_to?(:timeout) ? limiter.timeout : 0
31
+ def timeout
32
+ limiter.respond_to?(:timeout) ? limiter.timeout : nil
33
33
  end
34
34
 
35
35
  end
@@ -1,5 +1,5 @@
1
1
  module Berater
2
- class RateLimiter < BaseLimiter
2
+ class RateLimiter < Limiter
3
3
 
4
4
  class Overrated < Overloaded; end
5
5
 
@@ -28,6 +28,7 @@ module Berater
28
28
  case @interval
29
29
  when Integer
30
30
  raise ArgumentError, "interval must be >= 0" unless @interval >= 0
31
+ @interval_sec = @interval
31
32
  when String
32
33
  @interval = @interval.to_sym
33
34
  when Symbol
@@ -38,28 +39,29 @@ module Berater
38
39
  if @interval.is_a? Symbol
39
40
  case @interval
40
41
  when :sec, :second, :seconds
41
- @interval = 1
42
+ @interval = :second
43
+ @interval_sec = 1
42
44
  when :min, :minute, :minutes
43
- @interval = 60
45
+ @interval = :minute
46
+ @interval_sec = 60
44
47
  when :hour, :hours
45
- @interval = 60 * 60
48
+ @interval = :hour
49
+ @interval_sec = 60 * 60
46
50
  else
47
51
  raise ArgumentError, "unexpected interval value: #{interval}"
48
52
  end
49
53
  end
50
-
51
- @interval
52
54
  end
53
55
 
54
56
  def limit
55
57
  ts = Time.now.to_i
56
58
 
57
59
  # bucket into time slot
58
- rkey = "%s:%d" % [ cache_key(key), ts - ts % @interval ]
60
+ rkey = "%s:%d" % [ cache_key(key), ts - ts % @interval_sec ]
59
61
 
60
62
  count, _ = redis.multi do
61
63
  redis.incr rkey
62
- redis.expire rkey, @interval * 2
64
+ redis.expire rkey, @interval_sec * 2
63
65
  end
64
66
 
65
67
  raise Overrated if count > @count
@@ -77,6 +79,20 @@ module Berater
77
79
  end
78
80
  end
79
81
 
82
+ def to_s
83
+ msg = if @interval.is_a? Integer
84
+ if @interval == 1
85
+ "every second"
86
+ else
87
+ "every #{@interval} seconds"
88
+ end
89
+ else
90
+ "per #{@interval}"
91
+ end
92
+
93
+ "#<#{self.class}(#{key}: #{count} #{msg})>"
94
+ end
95
+
80
96
  end
81
97
  end
82
98
 
@@ -0,0 +1,12 @@
1
+ require 'berater'
2
+ require 'berater/rspec/matchers'
3
+ require 'berater/test_mode'
4
+ require 'rspec'
5
+
6
+ RSpec.configure do |config|
7
+ config.include(BeraterMatchers)
8
+
9
+ config.after do
10
+ Berater.expunge rescue nil
11
+ end
12
+ end
@@ -0,0 +1,60 @@
1
+ module BeraterMatchers
2
+ class Overloaded
3
+ def initialize(type)
4
+ @type = type
5
+ end
6
+
7
+ def supports_block_expectations?
8
+ true
9
+ end
10
+
11
+ def matches?(obj)
12
+ begin
13
+ case obj
14
+ when Proc
15
+ # eg. expect { ... }.to be_overrated
16
+ res = obj.call
17
+
18
+ if res.is_a? Berater::Limiter
19
+ # eg. expect { Berater.new(...) }.to be_overrated
20
+ res.limit {}
21
+ end
22
+ when Berater::Limiter
23
+ # eg. expect(Berater.new(...)).to be_overrated
24
+ obj.limit {}
25
+ end
26
+
27
+ false
28
+ rescue @type
29
+ true
30
+ end
31
+ end
32
+
33
+ # def description
34
+ # it { expect { Berater.new(:inhibitor) }.not_to be_overrated }
35
+
36
+ def failure_message
37
+ "expected #{@type} to be raised"
38
+ end
39
+
40
+ def failure_message_when_negated
41
+ "did not expect #{@type} to be raised"
42
+ end
43
+ end
44
+
45
+ def be_overloaded
46
+ Overloaded.new(Berater::Overloaded)
47
+ end
48
+
49
+ def be_overrated
50
+ Overloaded.new(Berater::RateLimiter::Overrated)
51
+ end
52
+
53
+ def be_incapacitated
54
+ Overloaded.new(Berater::ConcurrencyLimiter::Incapacitated)
55
+ end
56
+
57
+ def be_inhibited
58
+ Overloaded.new(Berater::Inhibitor::Inhibited)
59
+ end
60
+ end
@@ -0,0 +1,43 @@
1
+ require 'berater'
2
+
3
+ module Berater
4
+ extend self
5
+
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
12
+
13
+ @test_mode = mode
14
+ end
15
+
16
+ class Limiter
17
+ def self.new(*args, **opts)
18
+ return super unless Berater.test_mode
19
+
20
+ # chose a stub class with desired behavior
21
+ stub_klass = case Berater.test_mode
22
+ when :pass
23
+ Berater::Unlimiter
24
+ when :fail
25
+ Berater::Inhibitor
26
+ end
27
+
28
+ # don't stub self
29
+ return super if self < stub_klass
30
+
31
+ # swap out limit method with stub
32
+ super.tap do |instance|
33
+ stub = stub_klass.allocate
34
+ stub.send(:initialize, *args, **opts)
35
+
36
+ instance.define_singleton_method(:limit) do |&block|
37
+ stub.limit(&block)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ end
@@ -1,13 +1,12 @@
1
1
  module Berater
2
- class Unlimiter < BaseLimiter
2
+ class Unlimiter < Limiter
3
3
 
4
4
  def initialize(key = :unlimiter, *args, **opts)
5
5
  super(key, **opts)
6
6
  end
7
7
 
8
8
  def limit
9
- count = redis.incr(cache_key('count'))
10
- lock = Lock.new(self, count, count)
9
+ lock = Lock.new(self, 0, 0)
11
10
 
12
11
  if block_given?
13
12
  begin
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -120,6 +120,52 @@ describe Berater do
120
120
  expect {|b| Berater(:key, :concurrency, 1, &b) }.to yield_control
121
121
  end
122
122
  end
123
+
124
+ context 'with DSL' do
125
+ it 'instatiates an Unlimiter' do
126
+ limiter = Berater.new(:key) { unlimited }
127
+ expect(limiter).to be_a Berater::Unlimiter
128
+ expect(limiter.key).to be :key
129
+ end
130
+
131
+ it 'instatiates an Inhibiter' do
132
+ limiter = Berater.new(:key) { inhibited }
133
+ expect(limiter).to be_a Berater::Inhibitor
134
+ expect(limiter.key).to be :key
135
+ end
136
+
137
+ it 'instatiates a RateLimiter' do
138
+ limiter = Berater.new(:key) { 1.per second }
139
+ expect(limiter).to be_a Berater::RateLimiter
140
+ expect(limiter.key).to be :key
141
+ expect(limiter.count).to be 1
142
+ expect(limiter.interval).to be :second
143
+ end
144
+
145
+ it 'instatiates a ConcurrencyLimiter' do
146
+ limiter = Berater.new(:key, timeout: 2) { 1.at_once }
147
+ expect(limiter).to be_a Berater::ConcurrencyLimiter
148
+ expect(limiter.key).to be :key
149
+ expect(limiter.capacity).to be 1
150
+ expect(limiter.timeout).to be 2
151
+ end
152
+
153
+ it 'does not accept mode/args and dsl block' do
154
+ expect {
155
+ Berater.new(:key, :rate) { 1.per second }
156
+ }.to raise_error(ArgumentError)
157
+
158
+ expect {
159
+ Berater.new(:key, :concurrency, 2) { 3.at_once }
160
+ }.to raise_error(ArgumentError)
161
+ end
162
+
163
+ it 'requires either mode or dsl block' do
164
+ expect {
165
+ Berater.new(:key)
166
+ }.to raise_error(ArgumentError)
167
+ end
168
+ end
123
169
  end
124
170
 
125
171
  end
@@ -1,4 +1,7 @@
1
1
  describe Berater::ConcurrencyLimiter do
2
+ it_behaves_like 'a limiter', described_class.new(:key, 1)
3
+ it_behaves_like 'a limiter', described_class.new(:key, 1, timeout: 1)
4
+
2
5
  describe '.new' do
3
6
  let(:limiter) { described_class.new(:key, 1) }
4
7
 
@@ -61,7 +64,7 @@ describe Berater::ConcurrencyLimiter do
61
64
  end
62
65
 
63
66
  describe '#limit' do
64
- let(:limiter) { described_class.new(:key, 2, timeout: 1) }
67
+ let(:limiter) { described_class.new(:key, 2) }
65
68
 
66
69
  it 'works' do
67
70
  expect {|b| limiter.limit(&b) }.to yield_control
@@ -85,18 +88,6 @@ describe Berater::ConcurrencyLimiter do
85
88
  expect(limiter).to be_incapacitated
86
89
  end
87
90
 
88
- it 'times out locks' do
89
- expect(limiter.limit).to be_a Berater::Lock
90
- expect(limiter.limit).to be_a Berater::Lock
91
- expect(limiter).to be_incapacitated
92
-
93
- Timecop.travel(1)
94
-
95
- expect(limiter.limit).to be_a Berater::Lock
96
- expect(limiter.limit).to be_a Berater::Lock
97
- expect(limiter).to be_incapacitated
98
- end
99
-
100
91
  context 'with capacity 0' do
101
92
  let(:limiter) { described_class.new(:key, 0) }
102
93
 
@@ -171,4 +162,17 @@ describe Berater::ConcurrencyLimiter do
171
162
  end
172
163
  end
173
164
 
165
+ describe '#to_s' do
166
+ def check(capacity, expected)
167
+ expect(
168
+ described_class.new(:key, capacity).to_s
169
+ ).to match(expected)
170
+ end
171
+
172
+ it 'works' do
173
+ check(1, /1 at a time/)
174
+ check(3, /3 at a time/)
175
+ end
176
+ end
177
+
174
178
  end
File without changes
@@ -1,4 +1,5 @@
1
1
  describe Berater::RateLimiter do
2
+ it_behaves_like 'a limiter', Berater.new(:key, :rate, 3, :second)
2
3
 
3
4
  describe '.new' do
4
5
  let(:limiter) { described_class.new(:key, 1, :second) }
@@ -6,7 +7,7 @@ describe Berater::RateLimiter do
6
7
  it 'initializes' do
7
8
  expect(limiter.key).to be :key
8
9
  expect(limiter.count).to eq 1
9
- expect(limiter.interval).to eq 1
10
+ expect(limiter.interval).to eq :second
10
11
  end
11
12
 
12
13
  it 'has default values' do
@@ -51,22 +52,22 @@ describe Berater::RateLimiter do
51
52
  end
52
53
 
53
54
  context 'with symbols' do
54
- it { expect_interval(:sec, 1) }
55
- it { expect_interval(:second, 1) }
56
- it { expect_interval(:seconds, 1) }
55
+ it { expect_interval(:sec, :second) }
56
+ it { expect_interval(:second, :second) }
57
+ it { expect_interval(:seconds, :second) }
57
58
 
58
- it { expect_interval(:min, 60) }
59
- it { expect_interval(:minute, 60) }
60
- it { expect_interval(:minutes, 60) }
59
+ it { expect_interval(:min, :minute) }
60
+ it { expect_interval(:minute, :minute) }
61
+ it { expect_interval(:minutes, :minute) }
61
62
 
62
- it { expect_interval(:hour, 3600) }
63
- it { expect_interval(:hours, 3600) }
63
+ it { expect_interval(:hour, :hour) }
64
+ it { expect_interval(:hours, :hour) }
64
65
  end
65
66
 
66
67
  context 'with strings' do
67
- it { expect_interval('sec', 1) }
68
- it { expect_interval('minute', 60) }
69
- it { expect_interval('hours', 3600) }
68
+ it { expect_interval('sec', :second) }
69
+ it { expect_interval('minute', :minute) }
70
+ it { expect_interval('hours', :hour) }
70
71
  end
71
72
 
72
73
  context 'with erroneous values' do
@@ -80,6 +81,17 @@ describe Berater::RateLimiter do
80
81
  it { expect_bad_interval(:secondz) }
81
82
  it { expect_bad_interval('huor') }
82
83
  end
84
+
85
+ context 'interprets values' do
86
+ def expect_sec(interval, expected)
87
+ limiter = described_class.new(:key, 1, interval)
88
+ expect(limiter.instance_variable_get(:@interval_sec)).to eq expected
89
+ end
90
+
91
+ it { expect_sec(:second, 1) }
92
+ it { expect_sec(:minute, 60) }
93
+ it { expect_sec(:hour, 3600) }
94
+ end
83
95
  end
84
96
 
85
97
  describe '#limit' do
@@ -140,4 +152,40 @@ describe Berater::RateLimiter do
140
152
  end
141
153
  end
142
154
 
155
+ describe '#to_s' do
156
+ def check(count, interval, expected)
157
+ expect(
158
+ described_class.new(:key, count, interval).to_s
159
+ ).to match(expected)
160
+ end
161
+
162
+ it 'works with symbols' do
163
+ check(1, :second, /1 per second/)
164
+ check(1, :minute, /1 per minute/)
165
+ check(1, :hour, /1 per hour/)
166
+ end
167
+
168
+ it 'works with strings' do
169
+ check(1, 'second', /1 per second/)
170
+ check(1, 'minute', /1 per minute/)
171
+ check(1, 'hour', /1 per hour/)
172
+ end
173
+
174
+ it 'normalizes' do
175
+ check(1, :sec, /1 per second/)
176
+ check(1, :seconds, /1 per second/)
177
+
178
+ check(1, :min, /1 per minute/)
179
+ check(1, :minutes, /1 per minute/)
180
+
181
+ check(1, :hours, /1 per hour/)
182
+ end
183
+
184
+ it 'works with integers' do
185
+ check(1, 1, /1 every second/)
186
+ check(1, 2, /1 every 2 seconds/)
187
+ check(2, 3, /2 every 3 seconds/)
188
+ end
189
+ end
190
+
143
191
  end
@@ -0,0 +1,183 @@
1
+ require 'berater/test_mode'
2
+
3
+ describe 'Berater.test_mode' do
4
+ after { Berater.test_mode = nil }
5
+
6
+ describe 'Unlimiter' do
7
+ let(:limiter) { Berater::Unlimiter.new }
8
+
9
+ context 'when test_mode = nil' do
10
+ before { Berater.test_mode = nil }
11
+
12
+ it { expect(limiter).to be_a Berater::Unlimiter }
13
+
14
+ it 'works per usual' do
15
+ expect {|block| limiter.limit(&block) }.to yield_control
16
+ 10.times { expect(limiter.limit).to be_a Berater::Lock }
17
+ end
18
+ end
19
+
20
+ context 'when test_mode = :pass' do
21
+ before { Berater.test_mode = :pass }
22
+
23
+ it { expect(limiter).to be_a Berater::Unlimiter }
24
+
25
+ it 'always works' do
26
+ expect {|block| limiter.limit(&block) }.to yield_control
27
+ 10.times { expect(limiter.limit).to be_a Berater::Lock }
28
+ end
29
+ end
30
+
31
+ context 'when test_mode = :fail' do
32
+ before { Berater.test_mode = :fail }
33
+
34
+ it { expect(limiter).to be_a Berater::Unlimiter }
35
+
36
+ it 'never works' do
37
+ expect { limiter }.to be_overloaded
38
+ end
39
+ end
40
+ end
41
+
42
+ describe 'Inhibitor' do
43
+ let(:limiter) { Berater::Inhibitor.new }
44
+
45
+ context 'when test_mode = nil' do
46
+ before { Berater.test_mode = nil }
47
+
48
+ it { expect(limiter).to be_a Berater::Inhibitor }
49
+
50
+ it 'works per usual' do
51
+ expect { limiter }.to be_overloaded
52
+ end
53
+ end
54
+
55
+ context 'when test_mode = :pass' do
56
+ before { Berater.test_mode = :pass }
57
+
58
+ it { expect(limiter).to be_a Berater::Inhibitor }
59
+
60
+ it 'always works' do
61
+ expect {|block| limiter.limit(&block) }.to yield_control
62
+ 10.times { expect(limiter.limit).to be_a Berater::Lock }
63
+ end
64
+ end
65
+
66
+ context 'when test_mode = :fail' do
67
+ before { Berater.test_mode = :fail }
68
+
69
+ it { expect(limiter).to be_a Berater::Inhibitor }
70
+
71
+ it 'never works' do
72
+ expect { limiter }.to be_overloaded
73
+ end
74
+ end
75
+ end
76
+
77
+ describe 'RateLimiter' do
78
+ let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
79
+
80
+ shared_examples 'a RateLimiter' do
81
+ it { expect(limiter).to be_a Berater::RateLimiter }
82
+
83
+ it 'checks arguments' do
84
+ expect {
85
+ Berater::RateLimiter.new(:key, 1)
86
+ }.to raise_error(ArgumentError)
87
+ end
88
+ end
89
+
90
+ context 'when test_mode = nil' do
91
+ before { Berater.test_mode = nil }
92
+
93
+ it_behaves_like 'a RateLimiter'
94
+
95
+ it 'works per usual' do
96
+ expect(limiter.redis).to receive(:multi).twice.and_call_original
97
+ expect(limiter.limit).to be_a Berater::Lock
98
+ expect { limiter.limit }.to be_overloaded
99
+ end
100
+
101
+ it 'yields per usual' do
102
+ expect {|block| limiter.limit(&block) }.to yield_control
103
+ end
104
+ end
105
+
106
+ context 'when test_mode = :pass' do
107
+ before { Berater.test_mode = :pass }
108
+
109
+ it_behaves_like 'a RateLimiter'
110
+
111
+ it 'always works and without calling redis' do
112
+ expect(limiter.redis).not_to receive(:multi)
113
+ expect {|block| limiter.limit(&block) }.to yield_control
114
+ 10.times { expect(limiter.limit).to be_a Berater::Lock }
115
+ end
116
+ end
117
+
118
+ context 'when test_mode = :fail' do
119
+ before { Berater.test_mode = :fail }
120
+
121
+ it_behaves_like 'a RateLimiter'
122
+
123
+ it 'never works and without calling redis' do
124
+ expect(limiter.redis).not_to receive(:multi)
125
+ expect { limiter }.to be_overloaded
126
+ end
127
+ end
128
+ end
129
+
130
+ describe 'ConcurrencyLimiter' do
131
+ let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
132
+
133
+ shared_examples 'a ConcurrencyLimiter' do
134
+ it { expect(limiter).to be_a Berater::ConcurrencyLimiter }
135
+
136
+ it 'checks arguments' do
137
+ expect {
138
+ Berater::ConcurrencyLimiter.new(:key, 1.0)
139
+ }.to raise_error(ArgumentError)
140
+ end
141
+ end
142
+
143
+ context 'when test_mode = nil' do
144
+ before { Berater.test_mode = nil }
145
+
146
+ it_behaves_like 'a ConcurrencyLimiter'
147
+
148
+ it 'works per usual' do
149
+ expect(limiter.redis).to receive(:eval).twice.and_call_original
150
+ expect(limiter.limit).to be_a Berater::Lock
151
+ expect { limiter.limit }.to be_overloaded
152
+ end
153
+
154
+ it 'yields per usual' do
155
+ expect {|block| limiter.limit(&block) }.to yield_control
156
+ end
157
+ end
158
+
159
+ context 'when test_mode = :pass' do
160
+ before { Berater.test_mode = :pass }
161
+
162
+ it_behaves_like 'a ConcurrencyLimiter'
163
+
164
+ it 'always works and without calling redis' do
165
+ expect(limiter.redis).not_to receive(:eval)
166
+ expect {|block| limiter.limit(&block) }.to yield_control
167
+ 10.times { expect(limiter.limit).to be_a Berater::Lock }
168
+ end
169
+ end
170
+
171
+ context 'when test_mode = :fail' do
172
+ before { Berater.test_mode = :fail }
173
+
174
+ it_behaves_like 'a ConcurrencyLimiter'
175
+
176
+ it 'never works and without calling redis' do
177
+ expect(limiter.redis).not_to receive(:eval)
178
+ expect { limiter }.to be_overloaded
179
+ end
180
+ end
181
+ end
182
+
183
+ end
@@ -1,4 +1,6 @@
1
1
  describe Berater::Unlimiter do
2
+ it_behaves_like 'a limiter', described_class.new
3
+
2
4
  describe '.new' do
3
5
  it 'initializes without any arguments or options' do
4
6
  expect(described_class.new).to be_a described_class
@@ -31,7 +33,4 @@ describe Berater::Unlimiter do
31
33
  end
32
34
  end
33
35
  end
34
-
35
- it_behaves_like 'a lock', described_class.new
36
-
37
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.2.0
4
+ version: 0.3.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-02-16 00:00:00.000000000 Z
11
+ date: 2021-02-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -115,20 +115,23 @@ extensions: []
115
115
  extra_rdoc_files: []
116
116
  files:
117
117
  - lib/berater.rb
118
- - lib/berater/base_limiter.rb
119
118
  - lib/berater/concurrency_limiter.rb
119
+ - lib/berater/dsl.rb
120
120
  - lib/berater/inhibitor.rb
121
+ - lib/berater/limiter.rb
121
122
  - lib/berater/lock.rb
122
123
  - lib/berater/rate_limiter.rb
124
+ - lib/berater/rspec.rb
125
+ - lib/berater/rspec/matchers.rb
126
+ - lib/berater/test_mode.rb
123
127
  - lib/berater/unlimiter.rb
124
128
  - lib/berater/version.rb
125
129
  - spec/berater_spec.rb
126
130
  - spec/concurrency_limiter_spec.rb
127
- - spec/concurrency_lock_spec.rb
128
131
  - spec/inhibitor_spec.rb
129
- - spec/matcher_spec.rb
132
+ - spec/matchers_spec.rb
130
133
  - spec/rate_limiter_spec.rb
131
- - spec/rate_lock_spec.rb
134
+ - spec/test_mode_spec.rb
132
135
  - spec/unlimiter_spec.rb
133
136
  homepage: https://github.com/dpep/berater_rb
134
137
  licenses:
@@ -155,10 +158,9 @@ specification_version: 4
155
158
  summary: Berater
156
159
  test_files:
157
160
  - spec/rate_limiter_spec.rb
158
- - spec/matcher_spec.rb
159
- - spec/rate_lock_spec.rb
161
+ - spec/matchers_spec.rb
162
+ - spec/test_mode_spec.rb
160
163
  - spec/concurrency_limiter_spec.rb
161
- - spec/concurrency_lock_spec.rb
162
164
  - spec/berater_spec.rb
163
165
  - spec/inhibitor_spec.rb
164
166
  - spec/unlimiter_spec.rb
@@ -1,39 +0,0 @@
1
- describe Berater::Lock do
2
- it_behaves_like 'a lock', Berater.new(:key, :concurrency, 3)
3
-
4
- let(:limiter) { Berater.new(:key, :concurrency, 3) }
5
-
6
- describe '#expired?' do
7
- let!(:lock) { limiter.limit }
8
-
9
- context 'when timeout is not set' do
10
- it { expect(limiter.timeout).to eq 0 }
11
-
12
- it 'never expires' do
13
- expect(lock.locked?).to be true
14
- expect(lock.expired?).to be false
15
-
16
- Timecop.travel(1_000)
17
-
18
- expect(lock.locked?).to be true
19
- expect(lock.expired?).to be false
20
- end
21
- end
22
-
23
- context 'when timeout is set and exceeded' do
24
- before { Timecop.travel(1) }
25
-
26
- let(:limiter) { Berater.new(:key, :concurrency, 3, timeout: 1) }
27
-
28
- it 'expires' do
29
- expect(lock.expired?).to be true
30
- expect(lock.locked?).to be false
31
- end
32
-
33
- it 'fails to release' do
34
- expect { lock.release }.to raise_error(RuntimeError, /expired/)
35
- end
36
- end
37
- end
38
-
39
- end
@@ -1,20 +0,0 @@
1
- describe Berater::Lock do
2
- it_behaves_like 'a lock', Berater.new(:key, :rate, 3, :second)
3
-
4
- let(:limiter) { Berater.new(:key, :rate, 3, :second) }
5
-
6
- describe '#expired?' do
7
- let!(:lock) { limiter.limit }
8
-
9
- it 'never expires' do
10
- expect(lock.locked?).to be true
11
- expect(lock.expired?).to be false
12
-
13
- Timecop.travel(1_000)
14
-
15
- expect(lock.locked?).to be true
16
- expect(lock.expired?).to be false
17
- end
18
- end
19
-
20
- end