berater 0.1.1 → 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.
@@ -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,18 +1,22 @@
1
1
  module Berater
2
- class Unlimiter < BaseLimiter
2
+ class Unlimiter < Limiter
3
3
 
4
- def initialize(*args, **opts)
5
- super(**opts)
4
+ def initialize(key = :unlimiter, *args, **opts)
5
+ super(key, **opts)
6
6
  end
7
7
 
8
- def limit(**opts, &block)
9
- unless opts.empty?
10
- return self.class.new(
11
- **options.merge(opts)
12
- ).limit(&block)
13
- end
8
+ def limit
9
+ lock = Lock.new(self, 0, 0)
14
10
 
15
- yield if block_given?
11
+ if block_given?
12
+ begin
13
+ yield lock
14
+ ensure
15
+ lock.release
16
+ end
17
+ else
18
+ lock
19
+ end
16
20
  end
17
21
 
18
22
  end
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.1.1'
2
+ VERSION = '0.3.0'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -9,10 +9,10 @@ describe Berater do
9
9
  describe '.configure' do
10
10
  it 'can be set via configure' do
11
11
  Berater.configure do |c|
12
- c.mode = :rate
12
+ c.redis = :redis
13
13
  end
14
14
 
15
- expect(Berater.mode).to eq :rate
15
+ expect(Berater.redis).to eq :redis
16
16
  end
17
17
  end
18
18
 
@@ -24,20 +24,13 @@ describe Berater do
24
24
  end
25
25
  end
26
26
 
27
- describe '.mode' do
28
- it 'validates inputs' do
29
- expect { Berater.mode = :foo }.to raise_error(ArgumentError)
30
- end
31
- end
32
-
33
- context 'unlimited mode' do
34
- before { Berater.mode = :unlimited }
35
-
36
- describe '.new' do
37
- let(:limiter) { Berater.new(:unlimited) }
27
+ describe '.new' do
28
+ context 'unlimited mode' do
29
+ let(:limiter) { Berater.new(:key, :unlimited) }
38
30
 
39
31
  it 'instantiates an Unlimiter' do
40
32
  expect(limiter).to be_a Berater::Unlimiter
33
+ expect(limiter.key).to be :key
41
34
  end
42
35
 
43
36
  it 'inherits redis' do
@@ -46,35 +39,22 @@ describe Berater do
46
39
 
47
40
  it 'accepts options' do
48
41
  redis = double('Redis')
49
- limiter = Berater.new(:unlimited, key: 'key', redis: redis)
50
- expect(limiter.key).to match(/key/)
42
+ limiter = Berater.new(:key, :unlimited, redis: redis)
51
43
  expect(limiter.redis).to be redis
52
44
  end
53
- end
54
-
55
- describe '.limit' do
56
- it 'works' do
57
- expect(Berater.limit).to be_nil
58
- end
59
45
 
60
- it 'yields' do
61
- expect {|b| Berater.limit(&b) }.to yield_control
62
- end
63
-
64
- it 'never limits' do
65
- 10.times { expect(Berater.limit { 123 } ).to eq 123 }
46
+ it 'works with convinience' do
47
+ expect(Berater).to receive(:new).and_return(limiter)
48
+ expect {|b| Berater(:key, :unlimited, &b) }.to yield_control
66
49
  end
67
50
  end
68
- end
69
51
 
70
- context 'inhibited mode' do
71
- before { Berater.mode = :inhibited }
72
-
73
- describe '.new' do
74
- let(:limiter) { Berater.new(:inhibited) }
52
+ context 'inhibited mode' do
53
+ let(:limiter) { Berater.new(:key, :inhibited) }
75
54
 
76
55
  it 'instantiates an Inhibitor' do
77
56
  expect(limiter).to be_a Berater::Inhibitor
57
+ expect(limiter.key).to be :key
78
58
  end
79
59
 
80
60
  it 'inherits redis' do
@@ -83,27 +63,22 @@ describe Berater do
83
63
 
84
64
  it 'accepts options' do
85
65
  redis = double('Redis')
86
- limiter = Berater.new(:inhibited, key: 'key', redis: redis)
87
- expect(limiter.key).to match(/key/)
66
+ limiter = Berater.new(:key, :inhibited, redis: redis)
88
67
  expect(limiter.redis).to be redis
89
68
  end
90
- end
91
69
 
92
- describe '.limit' do
93
- it 'always limits' do
94
- expect { Berater.limit }.to be_inhibited
70
+ it 'works with convinience' do
71
+ expect(Berater).to receive(:new).and_return(limiter)
72
+ expect { Berater(:key, :inhibited) }.to be_inhibited
95
73
  end
96
74
  end
97
- end
98
75
 
99
- context 'rate mode' do
100
- before { Berater.mode = :rate }
101
-
102
- describe '.limiter' do
103
- let(:limiter) { Berater.new(:rate, 1, :second) }
76
+ context 'rate mode' do
77
+ let(:limiter) { Berater.new(:key, :rate, 1, :second) }
104
78
 
105
79
  it 'instantiates a RateLimiter' do
106
80
  expect(limiter).to be_a Berater::RateLimiter
81
+ expect(limiter.key).to be :key
107
82
  end
108
83
 
109
84
  it 'inherits redis' do
@@ -112,44 +87,22 @@ describe Berater do
112
87
 
113
88
  it 'accepts options' do
114
89
  redis = double('Redis')
115
- limiter = Berater.new(:rate, 1, :second, key: 'key', redis: redis)
116
- expect(limiter.key).to match(/key/)
90
+ limiter = Berater.new(:key, :rate, 1, :second, redis: redis)
117
91
  expect(limiter.redis).to be redis
118
92
  end
119
- end
120
-
121
- describe '.limit' do
122
- it 'works' do
123
- expect(Berater.limit(1, :second)).to eq 1
124
- end
125
-
126
- it 'yields' do
127
- expect {|b| Berater.limit(2, :second, &b) }.to yield_control
128
- expect(Berater.limit(2, :second) { 123 }).to eq 123
129
- end
130
93
 
131
- it 'limits excessive calls' do
132
- expect(Berater.limit(1, :second)).to eq 1
133
- expect { Berater.limit(1, :second) }.to be_overrated
134
- end
135
-
136
- it 'accepts options' do
137
- redis = double('Redis')
138
- expect(redis).to receive(:multi)
139
-
140
- Berater.limit(1, :second, redis: redis) rescue nil
94
+ it 'works with convinience' do
95
+ expect(Berater).to receive(:new).and_return(limiter)
96
+ expect {|b| Berater(:key, :rate, 1, :second, &b) }.to yield_control
141
97
  end
142
98
  end
143
- end
144
-
145
- context 'concurrency mode' do
146
- before { Berater.mode = :concurrency }
147
99
 
148
- describe '.limiter' do
149
- let(:limiter) { Berater.new(:concurrency, 1) }
100
+ context 'concurrency mode' do
101
+ let(:limiter) { Berater.new(:key, :concurrency, 1) }
150
102
 
151
103
  it 'instantiates a ConcurrencyLimiter' do
152
104
  expect(limiter).to be_a Berater::ConcurrencyLimiter
105
+ expect(limiter.key).to be :key
153
106
  end
154
107
 
155
108
  it 'inherits redis' do
@@ -158,33 +111,59 @@ describe Berater do
158
111
 
159
112
  it 'accepts options' do
160
113
  redis = double('Redis')
161
- limiter = Berater.new(:concurrency, 1, key: 'key', redis: redis)
162
- expect(limiter.key).to match(/key/)
114
+ limiter = Berater.new(:key, :concurrency, 1, redis: redis)
163
115
  expect(limiter.redis).to be redis
164
116
  end
117
+
118
+ it 'works with convinience' do
119
+ expect(Berater).to receive(:new).and_return(limiter)
120
+ expect {|b| Berater(:key, :concurrency, 1, &b) }.to yield_control
121
+ end
165
122
  end
166
123
 
167
- describe '.limit' do
168
- it 'works (without blocks by returning a lock)' do
169
- lock = Berater.limit(1)
170
- expect(lock).to be_a Berater::ConcurrencyLimiter::Lock
171
- expect(lock.release).to be true
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
172
129
  end
173
130
 
174
- it 'yields' do
175
- expect {|b| Berater.limit(1, &b) }.to yield_control
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
176
135
  end
177
136
 
178
- it 'limits excessive calls' do
179
- Berater.limit(1)
180
- expect { Berater.limit(1) }.to be_incapacitated
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
181
143
  end
182
144
 
183
- it 'accepts options' do
184
- redis = double('Redis')
185
- expect(redis).to receive(:eval)
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
186
162
 
187
- Berater.limit(1, redis: redis) rescue nil
163
+ it 'requires either mode or dsl block' do
164
+ expect {
165
+ Berater.new(:key)
166
+ }.to raise_error(ArgumentError)
188
167
  end
189
168
  end
190
169
  end
@@ -1,22 +1,23 @@
1
1
  describe Berater::ConcurrencyLimiter do
2
- before { Berater.mode = :concurrency }
2
+ it_behaves_like 'a limiter', described_class.new(:key, 1)
3
+ it_behaves_like 'a limiter', described_class.new(:key, 1, timeout: 1)
3
4
 
4
5
  describe '.new' do
5
- let(:limiter) { described_class.new(1) }
6
+ let(:limiter) { described_class.new(:key, 1) }
6
7
 
7
8
  it 'initializes' do
9
+ expect(limiter.key).to be :key
8
10
  expect(limiter.capacity).to be 1
9
11
  end
10
12
 
11
13
  it 'has default values' do
12
- expect(limiter.key).to eq described_class.to_s
13
14
  expect(limiter.redis).to be Berater.redis
14
15
  end
15
16
  end
16
17
 
17
18
  describe '#capacity' do
18
19
  def expect_capacity(capacity)
19
- limiter = described_class.new(capacity)
20
+ limiter = described_class.new(:key, capacity)
20
21
  expect(limiter.capacity).to eq capacity
21
22
  end
22
23
 
@@ -27,7 +28,7 @@ describe Berater::ConcurrencyLimiter do
27
28
  context 'with erroneous values' do
28
29
  def expect_bad_capacity(capacity)
29
30
  expect do
30
- described_class.new(capacity)
31
+ described_class.new(:key, capacity)
31
32
  end.to raise_error ArgumentError
32
33
  end
33
34
 
@@ -40,7 +41,7 @@ describe Berater::ConcurrencyLimiter do
40
41
 
41
42
  describe '#timeout' do
42
43
  def expect_timeout(timeout)
43
- limiter = described_class.new(1, timeout: timeout)
44
+ limiter = described_class.new(:key, 1, timeout: timeout)
44
45
  expect(limiter.timeout).to eq timeout
45
46
  end
46
47
 
@@ -51,7 +52,7 @@ describe Berater::ConcurrencyLimiter do
51
52
  context 'with erroneous values' do
52
53
  def expect_bad_timeout(timeout)
53
54
  expect do
54
- described_class.new(1, timeout: timeout)
55
+ described_class.new(:key, 1, timeout: timeout)
55
56
  end.to raise_error ArgumentError
56
57
  end
57
58
 
@@ -63,126 +64,114 @@ describe Berater::ConcurrencyLimiter do
63
64
  end
64
65
 
65
66
  describe '#limit' do
66
- let(:limiter) { described_class.new(2, timeout: 1) }
67
+ let(:limiter) { described_class.new(:key, 2) }
67
68
 
68
69
  it 'works' do
69
70
  expect {|b| limiter.limit(&b) }.to yield_control
70
71
  end
71
72
 
72
- it 'works many times if workers complete and return locks' do
73
+ it 'works many times if workers release locks' do
73
74
  30.times do
74
75
  expect {|b| limiter.limit(&b) }.to yield_control
75
76
  end
77
+
78
+ 30.times do
79
+ lock = limiter.limit
80
+ lock.release
81
+ end
76
82
  end
77
83
 
78
84
  it 'limits excessive calls' do
79
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
80
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
85
+ expect(limiter.limit).to be_a Berater::Lock
86
+ expect(limiter.limit).to be_a Berater::Lock
81
87
 
82
- expect { limiter }.to be_incapacitated
88
+ expect(limiter).to be_incapacitated
83
89
  end
84
90
 
85
- it 'times out locks' do
86
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
87
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
88
- expect { limiter }.to be_incapacitated
91
+ context 'with capacity 0' do
92
+ let(:limiter) { described_class.new(:key, 0) }
89
93
 
90
- sleep(1)
91
-
92
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
93
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
94
- expect { limiter }.to be_incapacitated
94
+ it 'always fails' do
95
+ expect(limiter).to be_incapacitated
96
+ end
95
97
  end
96
98
  end
97
99
 
98
100
  context 'with same key, different limiters' do
99
- let(:limiter_one) { described_class.new(1) }
100
- let(:limiter_two) { described_class.new(1) }
101
+ let(:limiter_one) { described_class.new(:key, 1) }
102
+ let(:limiter_two) { described_class.new(:key, 1) }
101
103
 
102
104
  it { expect(limiter_one.key).to eq limiter_two.key }
103
105
 
104
106
  it 'works as expected' do
105
- expect(limiter_one.limit).to be_a Berater::ConcurrencyLimiter::Lock
107
+ expect(limiter_one.limit).to be_a Berater::Lock
106
108
 
107
- expect { limiter_one }.to be_incapacitated
108
- expect { limiter_two }.to be_incapacitated
109
- end
110
- end
111
-
112
- context 'with different keys, same limiter' do
113
- let(:limiter) { described_class.new(1) }
114
-
115
- it 'works as expected' do
116
- one_lock = limiter.limit(key: :one)
117
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
118
-
119
- expect { limiter.limit(key: :one) {} }.to be_incapacitated
120
- expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
121
-
122
- two_lock = limiter.limit(key: :two)
123
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
124
-
125
- expect { limiter.limit(key: :one) {} }.to be_incapacitated
126
- expect { limiter.limit(key: :two) {} }.to be_incapacitated
127
-
128
- one_lock.release
129
- expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
130
- expect { limiter.limit(key: :two) {} }.to be_incapacitated
131
-
132
- two_lock.release
133
- expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
134
- expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
109
+ expect(limiter_one).to be_incapacitated
110
+ expect(limiter_two).to be_incapacitated
135
111
  end
136
112
  end
137
113
 
138
114
  context 'with same key, different capacities' do
139
- let(:limiter_one) { described_class.new(1) }
140
- let(:limiter_two) { described_class.new(2) }
115
+ let(:limiter_one) { described_class.new(:key, 1) }
116
+ let(:limiter_two) { described_class.new(:key, 2) }
141
117
 
142
118
  it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
143
119
 
144
120
  it 'works as expected' do
145
121
  one_lock = limiter_one.limit
146
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
122
+ expect(one_lock).to be_a Berater::Lock
147
123
 
148
- expect { limiter_one }.to be_incapacitated
149
- expect { limiter_two }.not_to be_incapacitated
124
+ expect(limiter_one).to be_incapacitated
125
+ expect(limiter_two).not_to be_incapacitated
150
126
 
151
127
  two_lock = limiter_two.limit
152
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
128
+ expect(two_lock).to be_a Berater::Lock
153
129
 
154
- expect { limiter_one }.to be_incapacitated
155
- expect { limiter_two }.to be_incapacitated
130
+ expect(limiter_one).to be_incapacitated
131
+ expect(limiter_two).to be_incapacitated
156
132
 
157
133
  one_lock.release
158
- expect { limiter_one }.to be_incapacitated
159
- expect { limiter_two }.not_to be_incapacitated
134
+ expect(limiter_one).to be_incapacitated
135
+ expect(limiter_two).not_to be_incapacitated
160
136
 
161
137
  two_lock.release
162
- expect { limiter_one }.not_to be_incapacitated
163
- expect { limiter_two }.not_to be_incapacitated
138
+ expect(limiter_one).not_to be_incapacitated
139
+ expect(limiter_two).not_to be_incapacitated
164
140
  end
165
141
  end
166
142
 
167
143
  context 'with different keys, different limiters' do
168
- let(:limiter_one) { described_class.new(1, key: :one) }
169
- let(:limiter_two) { described_class.new(1, key: :two) }
144
+ let(:limiter_one) { described_class.new(:one, 1) }
145
+ let(:limiter_two) { described_class.new(:two, 1) }
170
146
 
171
147
  it 'works as expected' do
172
- expect { limiter_one }.not_to be_incapacitated
173
- expect { limiter_two }.not_to be_incapacitated
148
+ expect(limiter_one).not_to be_incapacitated
149
+ expect(limiter_two).not_to be_incapacitated
174
150
 
175
151
  one_lock = limiter_one.limit
176
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
152
+ expect(one_lock).to be_a Berater::Lock
177
153
 
178
- expect { limiter_one }.to be_incapacitated
179
- expect { limiter_two }.not_to be_incapacitated
154
+ expect(limiter_one).to be_incapacitated
155
+ expect(limiter_two).not_to be_incapacitated
180
156
 
181
157
  two_lock = limiter_two.limit
182
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
158
+ expect(two_lock).to be_a Berater::Lock
183
159
 
184
- expect { limiter_one }.to be_incapacitated
185
- expect { limiter_two }.to be_incapacitated
160
+ expect(limiter_one).to be_incapacitated
161
+ expect(limiter_two).to be_incapacitated
162
+ end
163
+ end
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/)
186
175
  end
187
176
  end
188
177