berater 0.7.1 → 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
data/spec/berater_spec.rb CHANGED
@@ -24,50 +24,33 @@ 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
+ describe '.limiters' do
28
+ subject { Berater.limiters }
30
29
 
31
- it 'instantiates an Unlimiter' do
32
- expect(limiter).to be_a Berater::Unlimiter
33
- expect(limiter.key).to be :key
34
- end
30
+ let(:limiter) { Berater(:key, 1) }
35
31
 
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
32
+ it 'provides access to predefined limiters' do
33
+ expect(Berater.limiters).to be_a Berater::LimiterSet
45
34
  end
46
35
 
47
- context 'Inhibitor mode' do
48
- let(:limiter) { Berater.new(:key, 0) }
36
+ it 'resets with Berater' do
37
+ subject << limiter
38
+ is_expected.not_to be_empty
49
39
 
50
- it 'instantiates an Inhibitor' do
51
- expect(limiter).to be_a Berater::Inhibitor
52
- expect(limiter.key).to be :key
53
- end
40
+ Berater.reset
41
+ is_expected.to be_empty
42
+ end
43
+ end
54
44
 
55
- it 'inherits redis' do
56
- expect(limiter.redis).to be Berater.redis
57
- end
45
+ shared_examples 'a Berater' do |klass, capacity, **opts|
46
+ describe '.new' do
47
+ let(:limiter) { Berater.new(:key, capacity, **opts) }
58
48
 
59
- it 'accepts options' do
60
- redis = double('Redis')
61
- limiter = Berater.new(:key, 0, redis: redis)
62
- expect(limiter.redis).to be redis
49
+ it 'instantiates the right class' do
50
+ expect(limiter).to be_a klass
63
51
  end
64
- end
65
52
 
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
53
+ it 'sets the key' do
71
54
  expect(limiter.key).to be :key
72
55
  end
73
56
 
@@ -75,60 +58,45 @@ describe Berater do
75
58
  expect(limiter.redis).to be Berater.redis
76
59
  end
77
60
 
78
- it 'accepts options' do
79
- redis = double('Redis')
80
- limiter = Berater.new(:key, 1, interval: :second, redis: redis)
61
+ it 'accepts an optional redis parameter' do
62
+ redis = double(Redis)
63
+ limiter = Berater.new(:key, capacity, **opts.merge(redis: redis))
81
64
  expect(limiter.redis).to be redis
82
65
  end
83
66
  end
84
67
 
85
- context 'concurrency mode' do
86
- let(:limiter) { Berater.new(:key, 1) }
68
+ describe 'Berater() convenience method' do
69
+ let(:limiter) { Berater(:key, capacity, **opts) }
87
70
 
88
- it 'instantiates a ConcurrencyLimiter' do
89
- expect(limiter).to be_a Berater::ConcurrencyLimiter
90
- expect(limiter.key).to be :key
71
+ it 'creates a limiter' do
72
+ expect(limiter).to be_a klass
91
73
  end
92
74
 
93
- it 'inherits redis' do
94
- expect(limiter.redis).to be Berater.redis
75
+ it 'creates an equivalent limiter' do
76
+ expect(limiter).to eq Berater.new(:key, capacity, **opts)
95
77
  end
96
78
 
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
79
+ context 'with a block' do
80
+ before { Berater.test_mode = :pass }
104
81
 
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
82
+ subject { Berater(:key, capacity, **opts) { 123 } }
111
83
 
112
- context 'with a block' do
113
84
  it 'creates a limiter and calls limit' do
114
- limiter = Berater(:key, capacity, **opts)
115
85
  expect(klass).to receive(:new).and_return(limiter)
116
86
  expect(limiter).to receive(:limit).and_call_original
87
+ subject
88
+ end
117
89
 
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
90
+ it 'yields' do
91
+ is_expected.to be 123
124
92
  end
125
93
  end
126
94
  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
95
  end
133
96
 
97
+ include_examples 'a Berater', Berater::ConcurrencyLimiter, 1, timeout: 1
98
+ include_examples 'a Berater', Berater::Inhibitor, 0
99
+ include_examples 'a Berater', Berater::RateLimiter, 1, interval: :second
100
+ include_examples 'a Berater', Berater::StaticLimiter, 1
101
+ include_examples 'a Berater', Berater::Unlimiter, Float::INFINITY
134
102
  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
@@ -0,0 +1,173 @@
1
+ describe Berater::LimiterSet do
2
+ subject { described_class.new }
3
+
4
+ let(:unlimiter) { Berater::Unlimiter.new }
5
+ let(:inhibitor) { Berater::Inhibitor.new }
6
+
7
+ describe '#each' do
8
+ it 'returns an Enumerator' do
9
+ expect(subject.each).to be_a Enumerator
10
+ end
11
+
12
+ it 'works with an empty set' do
13
+ expect(subject.each.to_a).to eq []
14
+ end
15
+
16
+ it 'returns elements' do
17
+ subject << unlimiter
18
+ expect(subject.each.to_a).to eq [ unlimiter ]
19
+ end
20
+ end
21
+
22
+ describe '#<<' do
23
+ it 'adds a limiter' do
24
+ subject << unlimiter
25
+ expect(subject.each.to_a).to eq [ unlimiter ]
26
+ end
27
+
28
+ it 'rejects things that are not limiters' do
29
+ expect {
30
+ subject << :foo
31
+ }.to raise_error(ArgumentError)
32
+ end
33
+
34
+ it 'updates existing keys' do
35
+ limiter = Berater::Unlimiter.new
36
+ expect(limiter).to eq unlimiter
37
+ expect(limiter).not_to be unlimiter
38
+
39
+ subject << unlimiter
40
+ subject << limiter
41
+
42
+ expect(subject.each.to_a).to eq [ limiter ]
43
+ end
44
+ end
45
+
46
+ describe '[]=' do
47
+ it 'adds a limiter' do
48
+ subject[:key] = unlimiter
49
+
50
+ expect(subject.each.to_a).to eq [ unlimiter ]
51
+ is_expected.to include :key
52
+ is_expected.to include unlimiter
53
+ end
54
+
55
+ it 'rejects things that are not limiters' do
56
+ expect {
57
+ subject[:key] = :foo
58
+ }.to raise_error(ArgumentError)
59
+ end
60
+ end
61
+
62
+ describe '#[]' do
63
+ it 'returns nil for missing keys' do
64
+ expect(subject[:key]).to be nil
65
+ expect(subject[nil]).to be nil
66
+ end
67
+
68
+ it 'retreives limiters' do
69
+ subject << unlimiter
70
+ expect(subject[unlimiter.key]).to be unlimiter
71
+ end
72
+ end
73
+
74
+ describe '#fetch' do
75
+ it 'raises for missing keys' do
76
+ expect {
77
+ subject.fetch(:key)
78
+ }.to raise_error(KeyError)
79
+
80
+ expect {
81
+ subject.fetch(nil)
82
+ }.to raise_error(KeyError)
83
+ end
84
+
85
+ it 'returns the default if provided' do
86
+ expect(subject.fetch(:key, unlimiter)).to be unlimiter
87
+ end
88
+
89
+ it 'calls the default proc if provided' do
90
+ expect {|block| subject.fetch(:key, &block) }.to yield_control
91
+ end
92
+
93
+ it 'retreives limiters' do
94
+ subject << unlimiter
95
+ expect(subject.fetch(unlimiter.key)).to be unlimiter
96
+ expect(subject.fetch(unlimiter.key, :default)).to be unlimiter
97
+ end
98
+ end
99
+
100
+ describe '#include?' do
101
+ before do
102
+ subject << unlimiter
103
+ end
104
+
105
+ it 'works with keys' do
106
+ is_expected.to include unlimiter.key
107
+ end
108
+
109
+ it 'works with limiters' do
110
+ is_expected.to include unlimiter
111
+ end
112
+
113
+ it 'works when target is missing' do
114
+ is_expected.not_to include inhibitor.key
115
+ is_expected.not_to include inhibitor
116
+ end
117
+ end
118
+
119
+ describe '#clear' do
120
+ it 'works when empty' do
121
+ subject.clear
122
+ end
123
+
124
+ it 'clears limiters' do
125
+ subject << unlimiter
126
+ is_expected.to include unlimiter
127
+
128
+ subject.clear
129
+ is_expected.not_to include unlimiter
130
+ end
131
+ end
132
+
133
+ describe '#count' do
134
+ it 'counts' do
135
+ expect(subject.count).to be 0
136
+
137
+ subject << unlimiter
138
+ expect(subject.count).to be 1
139
+ end
140
+ end
141
+
142
+ describe '#delete' do
143
+ it 'works when the target is missing' do
144
+ subject.delete(unlimiter)
145
+ subject.delete(unlimiter.key)
146
+ end
147
+
148
+ it 'works with keys' do
149
+ subject << unlimiter
150
+ is_expected.to include unlimiter
151
+
152
+ subject.delete(unlimiter.key)
153
+ is_expected.not_to include unlimiter
154
+ end
155
+
156
+ it 'works with limiters' do
157
+ subject << unlimiter
158
+ is_expected.to include unlimiter
159
+
160
+ subject.delete(unlimiter)
161
+ is_expected.not_to include unlimiter
162
+ end
163
+ end
164
+
165
+ describe '#empty?' do
166
+ it 'works' do
167
+ is_expected.to be_empty
168
+
169
+ subject << unlimiter
170
+ is_expected.not_to be_empty
171
+ end
172
+ end
173
+ end
data/spec/limiter_spec.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  describe Berater::Limiter do
2
- it 'can not be initialized' do
3
- expect { described_class.new }.to raise_error(NoMethodError)
2
+ describe '.new' do
3
+ it 'can only be called on subclasses' do
4
+ expect { described_class.new }.to raise_error(NoMethodError)
5
+ end
4
6
  end
5
7
 
6
8
  describe 'abstract methods' do
@@ -44,8 +46,49 @@ describe Berater::Limiter do
44
46
  expect {
45
47
  subject.limit(cost: -1)
46
48
  }.to raise_error(ArgumentError)
49
+
50
+ expect {
51
+ subject.limit(cost: Float::INFINITY)
52
+ }.to raise_error(ArgumentError)
53
+ end
54
+ end
55
+
56
+ context 'when Berater.redis is nil' do
57
+ let!(:redis) { Berater.redis }
58
+
59
+ before { Berater.redis = nil }
60
+
61
+ it 'works with Unlimiter since redis is not used' do
62
+ expect(subject.redis).to be nil
63
+ expect {|b| subject.limit(&b) }.to yield_control
64
+ end
65
+
66
+ it 'raises when redis is needed' do
67
+ limiter = Berater::RateLimiter.new(:key, 1, :second)
68
+ expect(limiter.redis).to be nil
69
+ expect { limiter.limit }.to raise_error(RuntimeError)
70
+ end
71
+
72
+ it 'works when redis is passed in' do
73
+ limiter = Berater::RateLimiter.new(:key, 1, :second, redis: redis)
74
+ expect {|b| limiter.limit(&b) }.to yield_control
75
+ end
76
+
77
+ it 'raises when redis is bogus' do
78
+ limiter = Berater::RateLimiter.new(:key, 1, :second, redis: :stub)
79
+ expect { limiter.limit }.to raise_error(RuntimeError)
47
80
  end
48
81
  end
82
+
83
+ it 'releases the lock even when limited code raises an error' do
84
+ lock = Berater::Lock.new(Float::INFINITY, 0)
85
+ expect(subject).to receive(:acquire_lock).and_return(lock)
86
+ expect(lock).to receive(:release)
87
+
88
+ expect {
89
+ subject.limit { raise 'fail' }
90
+ }.to raise_error(RuntimeError)
91
+ end
49
92
  end
50
93
 
51
94
  describe '#==' do
@@ -61,12 +104,6 @@ describe Berater::Limiter do
61
104
  )
62
105
  end
63
106
 
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
107
  it 'does not equal something different' do
71
108
  expect(limiter).not_to eq(
72
109
  Berater::RateLimiter.new(:key, 2, :second)
@@ -104,4 +141,56 @@ describe Berater::Limiter do
104
141
  end
105
142
  end
106
143
 
144
+ describe '#cache_key' do
145
+ subject { klass.new(:key).send(:cache_key) }
146
+
147
+ context 'with Unlimiter' do
148
+ let(:klass) { Berater::Unlimiter }
149
+
150
+ it do
151
+ is_expected.to eq 'Berater:Unlimiter:key'
152
+ end
153
+ end
154
+
155
+ context 'with custom limiter' do
156
+ MyLimiter = Class.new(Berater::Unlimiter)
157
+
158
+ let(:klass) { MyLimiter }
159
+
160
+ it 'adds Berater prefix' do
161
+ is_expected.to eq 'Berater:MyLimiter:key'
162
+ end
163
+ end
164
+ end
165
+
166
+ describe '.cache_key' do
167
+ subject { klass.send(:cache_key, :key) }
168
+
169
+ context 'with Unlimiter' do
170
+ let(:klass) { Berater::Unlimiter }
171
+
172
+ it do
173
+ is_expected.to eq 'Berater:Unlimiter:key'
174
+ end
175
+ end
176
+
177
+ context 'with custom limiter' do
178
+ MyLimiter = Class.new(Berater::Unlimiter)
179
+
180
+ let(:klass) { MyLimiter }
181
+
182
+ it 'adds Berater prefix' do
183
+ is_expected.to eq 'Berater:MyLimiter:key'
184
+ end
185
+ end
186
+ end
187
+
188
+ describe '.inherited' do
189
+ it 'creates convenience methods' do
190
+ expect(Berater.method(:Unlimiter)).to be_a Method
191
+ expect(Berater::Unlimiter()).to be_a Berater::Unlimiter
192
+ expect {|b| Berater::Unlimiter(&b) }.to yield_control
193
+ end
194
+ end
195
+
107
196
  end
@@ -93,5 +93,4 @@ describe Berater::LuaScript do
93
93
  it { subject }
94
94
  end
95
95
  end
96
-
97
96
  end
@@ -0,0 +1,110 @@
1
+ class Meddler
2
+ def call(*args, **kwargs)
3
+ yield
4
+ end
5
+ end
6
+
7
+ describe 'Berater.middleware' do
8
+ subject { Berater.middleware }
9
+
10
+ describe 'adding middleware' do
11
+ after { is_expected.to include Meddler }
12
+
13
+ it 'can be done inline' do
14
+ Berater.middleware.use Meddler
15
+ end
16
+
17
+ it 'can be done with a block' do
18
+ Berater.middleware do
19
+ use Meddler
20
+ end
21
+ end
22
+ end
23
+
24
+ it 'resets along with Berater' do
25
+ Berater.middleware.use Meddler
26
+ is_expected.to include Meddler
27
+
28
+ Berater.reset
29
+ is_expected.to be_empty
30
+ end
31
+
32
+ describe 'Berater::Limiter#limit' do
33
+ let(:middleware) { Meddler.new }
34
+ let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
35
+
36
+ before do
37
+ expect(Meddler).to receive(:new).and_return(middleware).at_least(1)
38
+ Berater.middleware.use Meddler
39
+ end
40
+
41
+ it 'calls the middleware' do
42
+ expect(middleware).to receive(:call)
43
+ limiter.limit
44
+ end
45
+
46
+ it 'calls the middleware, passing the limiter and options' do
47
+ expect(middleware).to receive(:call).with(
48
+ limiter,
49
+ hash_including(:capacity, :cost)
50
+ )
51
+
52
+ limiter.limit
53
+ end
54
+
55
+ context 'when used per ususual' do
56
+ before do
57
+ expect(middleware).to receive(:call).and_call_original.at_least(1)
58
+ end
59
+
60
+ it 'still works inline' do
61
+ expect(limiter.limit).to be_a Berater::Lock
62
+ end
63
+
64
+ it 'still works in block mode' do
65
+ expect(limiter.limit { 123 }).to be 123
66
+ end
67
+
68
+ it 'still has limits' do
69
+ limiter.limit
70
+ expect(limiter).to be_overloaded
71
+ end
72
+ end
73
+
74
+ context 'when middleware meddles' do
75
+ it 'can change the capacity' do
76
+ expect(middleware).to receive(:call) do |limiter, **opts, &block|
77
+ opts[:capacity] = 0
78
+ block.call(limiter, **opts)
79
+ end
80
+
81
+ expect { limiter.limit }.to be_overloaded
82
+ end
83
+
84
+ it 'can change the cost' do
85
+ expect(middleware).to receive(:call) do |limiter, **opts, &block|
86
+ opts[:cost] = 2
87
+ block.call(limiter, **opts)
88
+ end
89
+
90
+ expect { limiter.limit }.to be_overloaded
91
+ end
92
+
93
+ it 'can change the limiter' do
94
+ other_limiter = Berater::Inhibitor.new
95
+
96
+ expect(middleware).to receive(:call) do |limiter, **opts, &block|
97
+ block.call(other_limiter, **opts)
98
+ end
99
+ expect(other_limiter).to receive(:acquire_lock).and_call_original
100
+
101
+ expect { limiter.limit }.to be_overloaded
102
+ end
103
+
104
+ it 'can abort by not yielding' do
105
+ expect(middleware).to receive(:call)
106
+ expect(limiter.limit).to be nil
107
+ end
108
+ end
109
+ end
110
+ 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