berater 0.7.1 → 0.10.1

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.
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