berater 0.1.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.1.3'
2
+ VERSION = '0.5.0'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -7,12 +7,12 @@ describe Berater do
7
7
  it { is_expected.to respond_to :configure }
8
8
 
9
9
  describe '.configure' do
10
- it 'can be set via configure' do
10
+ it 'is used with a block' 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 be :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, Float::INFINITY) }
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,17 @@ 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, Float::INFINITY, redis: redis)
51
43
  expect(limiter.redis).to be redis
52
44
  end
53
45
  end
54
46
 
55
- describe '.limit' do
56
- it 'works' do
57
- expect(Berater.limit).to be_nil
58
- end
59
-
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 }
66
- end
67
- end
68
- end
69
-
70
- context 'inhibited mode' do
71
- before { Berater.mode = :inhibited }
72
-
73
- describe '.new' do
74
- let(:limiter) { Berater.new(:inhibited) }
47
+ context 'inhibited mode' do
48
+ let(:limiter) { Berater.new(:key, 0) }
75
49
 
76
50
  it 'instantiates an Inhibitor' do
77
51
  expect(limiter).to be_a Berater::Inhibitor
52
+ expect(limiter.key).to be :key
78
53
  end
79
54
 
80
55
  it 'inherits redis' do
@@ -83,27 +58,17 @@ describe Berater do
83
58
 
84
59
  it 'accepts options' do
85
60
  redis = double('Redis')
86
- limiter = Berater.new(:inhibited, key: 'key', redis: redis)
87
- expect(limiter.key).to match(/key/)
61
+ limiter = Berater.new(:key, 0, redis: redis)
88
62
  expect(limiter.redis).to be redis
89
63
  end
90
64
  end
91
65
 
92
- describe '.limit' do
93
- it 'always limits' do
94
- expect { Berater.limit }.to be_inhibited
95
- end
96
- end
97
- end
98
-
99
- context 'rate mode' do
100
- before { Berater.mode = :rate }
101
-
102
- describe '.limiter' do
103
- let(:limiter) { Berater.new(:rate, 1, :second) }
66
+ context 'rate mode' do
67
+ let(:limiter) { Berater.new(:key, 1, :second) }
104
68
 
105
69
  it 'instantiates a RateLimiter' do
106
70
  expect(limiter).to be_a Berater::RateLimiter
71
+ expect(limiter.key).to be :key
107
72
  end
108
73
 
109
74
  it 'inherits redis' do
@@ -112,44 +77,17 @@ describe Berater do
112
77
 
113
78
  it 'accepts options' do
114
79
  redis = double('Redis')
115
- limiter = Berater.new(:rate, 1, :second, key: 'key', redis: redis)
116
- expect(limiter.key).to match(/key/)
80
+ limiter = Berater.new(:key, 1, :second, redis: redis)
117
81
  expect(limiter.redis).to be redis
118
82
  end
119
83
  end
120
84
 
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
-
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
141
- end
142
- end
143
- end
144
-
145
- context 'concurrency mode' do
146
- before { Berater.mode = :concurrency }
147
-
148
- describe '.limiter' do
149
- let(:limiter) { Berater.new(:concurrency, 1) }
85
+ context 'concurrency mode' do
86
+ let(:limiter) { Berater.new(:key, 1) }
150
87
 
151
88
  it 'instantiates a ConcurrencyLimiter' do
152
89
  expect(limiter).to be_a Berater::ConcurrencyLimiter
90
+ expect(limiter.key).to be :key
153
91
  end
154
92
 
155
93
  it 'inherits redis' do
@@ -158,35 +96,39 @@ describe Berater do
158
96
 
159
97
  it 'accepts options' do
160
98
  redis = double('Redis')
161
- limiter = Berater.new(:concurrency, 1, key: 'key', redis: redis)
162
- expect(limiter.key).to match(/key/)
99
+ limiter = Berater.new(:key, 1, redis: redis)
163
100
  expect(limiter.redis).to be redis
164
101
  end
165
102
  end
103
+ end
166
104
 
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
172
- end
173
-
174
- it 'yields' do
175
- expect {|b| Berater.limit(1, &b) }.to yield_control
176
- end
177
-
178
- it 'limits excessive calls' do
179
- Berater.limit(1)
180
- expect { Berater.limit(1) }.to be_incapacitated
105
+ describe 'Berater() - convenience method' do
106
+ RSpec.shared_examples 'test convenience' do |klass, *args|
107
+ it 'creates a limiter' do
108
+ limiter = Berater(:key, *args)
109
+ expect(limiter).to be_a klass
181
110
  end
182
111
 
183
- it 'accepts options' do
184
- redis = double('Redis')
185
- expect(redis).to receive(:eval)
112
+ context 'with a block' do
113
+ it 'creates a limiter and calls limit' do
114
+ limiter = Berater(:key, *args)
115
+ expect(klass).to receive(:new).and_return(limiter)
116
+ expect(limiter).to receive(:limit).and_call_original
186
117
 
187
- Berater.limit(1, redis: redis) rescue nil
118
+ begin
119
+ res = Berater(:key, *args) { true }
120
+ expect(res).to be true
121
+ rescue Berater::Overloaded
122
+ expect(klass).to be Berater::Inhibitor
123
+ end
124
+ end
188
125
  end
189
126
  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, :second
131
+ include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
190
132
  end
191
133
 
192
134
  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
 
@@ -39,150 +40,213 @@ describe Berater::ConcurrencyLimiter do
39
40
  end
40
41
 
41
42
  describe '#timeout' do
42
- def expect_timeout(timeout)
43
- limiter = described_class.new(1, timeout: timeout)
44
- expect(limiter.timeout).to eq timeout
45
- end
46
-
47
- it { expect_timeout(0) }
48
- it { expect_timeout(1) }
49
- it { expect_timeout(10_000) }
43
+ # see spec/utils_spec.rb
50
44
 
51
- context 'with erroneous values' do
52
- def expect_bad_timeout(timeout)
53
- expect do
54
- described_class.new(1, timeout: timeout)
55
- end.to raise_error ArgumentError
56
- end
45
+ it 'saves the interval in original and microsecond format' do
46
+ limiter = described_class.new(:key, 1, timeout: 3)
47
+ expect(limiter.timeout).to be 3
48
+ expect(limiter.instance_variable_get(:@timeout_usec)).to be (3 * 10**6)
49
+ end
57
50
 
58
- it { expect_bad_timeout(0.5) }
59
- it { expect_bad_timeout(-1) }
60
- it { expect_bad_timeout('1') }
61
- it { expect_bad_timeout(:one) }
51
+ it 'handles infinity' do
52
+ limiter = described_class.new(:key, 1, timeout: Float::INFINITY)
53
+ expect(limiter.timeout).to be Float::INFINITY
54
+ expect(limiter.instance_variable_get(:@timeout_usec)).to be 0
62
55
  end
63
56
  end
64
57
 
65
58
  describe '#limit' do
66
- let(:limiter) { described_class.new(2, timeout: 1) }
59
+ let(:limiter) { described_class.new(:key, 2, timeout: 30) }
67
60
 
68
61
  it 'works' do
69
62
  expect {|b| limiter.limit(&b) }.to yield_control
70
63
  end
71
64
 
72
- it 'works many times if workers complete and return locks' do
65
+ it 'works many times if workers release locks' do
73
66
  30.times do
74
67
  expect {|b| limiter.limit(&b) }.to yield_control
75
68
  end
69
+
70
+ 30.times do
71
+ lock = limiter.limit
72
+ lock.release
73
+ end
76
74
  end
77
75
 
78
76
  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
77
+ expect(limiter.limit).to be_a Berater::Lock
78
+ expect(limiter.limit).to be_a Berater::Lock
81
79
 
82
- expect { limiter }.to be_incapacitated
80
+ expect(limiter).to be_incapacitated
83
81
  end
84
82
 
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
89
-
90
- Timecop.travel(1)
83
+ context 'with capacity 0' do
84
+ let(:limiter) { described_class.new(:key, 0) }
91
85
 
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
86
+ it 'always fails' do
87
+ expect(limiter).to be_incapacitated
88
+ end
95
89
  end
96
- end
97
90
 
98
- context 'with same key, different limiters' do
99
- let(:limiter_one) { described_class.new(1) }
100
- let(:limiter_two) { described_class.new(1) }
91
+ it 'limit resets over time' do
92
+ 2.times { limiter.limit }
93
+ expect(limiter).to be_incapacitated
94
+
95
+ Timecop.freeze(30)
96
+
97
+ 2.times { limiter.limit }
98
+ expect(limiter).to be_incapacitated
99
+ end
101
100
 
102
- it { expect(limiter_one.key).to eq limiter_two.key }
101
+ it 'limit resets with millisecond precision' do
102
+ 2.times { limiter.limit }
103
+ expect(limiter).to be_incapacitated
103
104
 
104
- it 'works as expected' do
105
- expect(limiter_one.limit).to be_a Berater::ConcurrencyLimiter::Lock
105
+ # travel forward to just before first lock times out
106
+ Timecop.freeze(29.999)
107
+ expect(limiter).to be_incapacitated
106
108
 
107
- expect { limiter_one }.to be_incapacitated
108
- expect { limiter_two }.to be_incapacitated
109
+ # traveling one more millisecond will decrement the count
110
+ Timecop.freeze(0.001)
111
+ 2.times { limiter.limit }
112
+ expect(limiter).to be_incapacitated
109
113
  end
110
- end
111
114
 
112
- context 'with different keys, same limiter' do
113
- let(:limiter) { described_class.new(1) }
115
+ context 'with cost parameter' do
116
+ it { expect { limiter.limit(cost: 4) }.to be_incapacitated }
117
+
118
+ it 'works within limit' do
119
+ limiter.limit(cost: 2)
120
+ expect(limiter).to be_incapacitated
121
+ end
122
+
123
+ it 'releases full cost' do
124
+ lock = limiter.limit(cost: 2)
125
+ expect(limiter).to be_incapacitated
126
+
127
+ lock.release
128
+ expect(limiter).not_to be_incapacitated
114
129
 
115
- it 'works as expected' do
116
- one_lock = limiter.limit(key: :one)
117
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
130
+ lock = limiter.limit(cost: 2)
131
+ expect(limiter).to be_incapacitated
132
+ end
118
133
 
119
- expect { limiter.limit(key: :one) {} }.to be_incapacitated
120
- expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
134
+ it 'respects timeout' do
135
+ limiter.limit(cost: 2)
136
+ expect(limiter).to be_incapacitated
121
137
 
122
- two_lock = limiter.limit(key: :two)
123
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
138
+ Timecop.freeze(30)
139
+ expect(limiter).not_to be_incapacitated
124
140
 
125
- expect { limiter.limit(key: :one) {} }.to be_incapacitated
126
- expect { limiter.limit(key: :two) {} }.to be_incapacitated
141
+ limiter.limit(cost: 2)
142
+ expect(limiter).to be_incapacitated
143
+ end
127
144
 
128
- one_lock.release
129
- expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
130
- expect { limiter.limit(key: :two) {} }.to be_incapacitated
145
+ it 'accepts a dynamic capacity' do
146
+ limiter = described_class.new(:key, 1)
131
147
 
132
- two_lock.release
133
- expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
134
- expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
148
+ expect { limiter.limit(capacity: 0) }.to be_incapacitated
149
+ 5.times { limiter.limit(capacity: 10) }
150
+ expect { limiter }.to be_incapacitated
151
+ end
152
+ end
153
+
154
+ context 'with same key, different limiters' do
155
+ let(:limiter_one) { described_class.new(:key, 1) }
156
+ let(:limiter_two) { described_class.new(:key, 1) }
157
+
158
+ it { expect(limiter_one.key).to eq limiter_two.key }
159
+
160
+ it 'works as expected' do
161
+ expect(limiter_one.limit).to be_a Berater::Lock
162
+
163
+ expect(limiter_one).to be_incapacitated
164
+ expect(limiter_two).to be_incapacitated
165
+ end
135
166
  end
136
- end
137
167
 
138
- context 'with same key, different capacities' do
139
- let(:limiter_one) { described_class.new(1) }
140
- let(:limiter_two) { described_class.new(2) }
168
+ context 'with same key, different capacities' do
169
+ let(:limiter_one) { described_class.new(:key, 1) }
170
+ let(:limiter_two) { described_class.new(:key, 2) }
141
171
 
142
- it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
172
+ it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
143
173
 
144
- it 'works as expected' do
145
- one_lock = limiter_one.limit
146
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
174
+ it 'works as expected' do
175
+ one_lock = limiter_one.limit
176
+ expect(one_lock).to be_a Berater::Lock
147
177
 
148
- expect { limiter_one }.to be_incapacitated
149
- expect { limiter_two }.not_to be_incapacitated
178
+ expect(limiter_one).to be_incapacitated
179
+ expect(limiter_two).not_to be_incapacitated
150
180
 
151
- two_lock = limiter_two.limit
152
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
181
+ two_lock = limiter_two.limit
182
+ expect(two_lock).to be_a Berater::Lock
153
183
 
154
- expect { limiter_one }.to be_incapacitated
155
- expect { limiter_two }.to be_incapacitated
184
+ expect(limiter_one).to be_incapacitated
185
+ expect(limiter_two).to be_incapacitated
156
186
 
157
- one_lock.release
158
- expect { limiter_one }.to be_incapacitated
159
- expect { limiter_two }.not_to be_incapacitated
187
+ one_lock.release
188
+ expect(limiter_one).to be_incapacitated
189
+ expect(limiter_two).not_to be_incapacitated
160
190
 
161
- two_lock.release
162
- expect { limiter_one }.not_to be_incapacitated
163
- expect { limiter_two }.not_to be_incapacitated
191
+ two_lock.release
192
+ expect(limiter_one).not_to be_incapacitated
193
+ expect(limiter_two).not_to be_incapacitated
194
+ end
164
195
  end
165
- end
166
196
 
167
- 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) }
197
+ context 'with different keys, different limiters' do
198
+ let(:limiter_one) { described_class.new(:one, 1) }
199
+ let(:limiter_two) { described_class.new(:two, 1) }
170
200
 
171
- it 'works as expected' do
172
- expect { limiter_one }.not_to be_incapacitated
173
- expect { limiter_two }.not_to be_incapacitated
201
+ it 'works as expected' do
202
+ expect(limiter_one).not_to be_incapacitated
203
+ expect(limiter_two).not_to be_incapacitated
174
204
 
175
- one_lock = limiter_one.limit
176
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
205
+ one_lock = limiter_one.limit
206
+ expect(one_lock).to be_a Berater::Lock
177
207
 
178
- expect { limiter_one }.to be_incapacitated
179
- expect { limiter_two }.not_to be_incapacitated
208
+ expect(limiter_one).to be_incapacitated
209
+ expect(limiter_two).not_to be_incapacitated
180
210
 
181
- two_lock = limiter_two.limit
182
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
211
+ two_lock = limiter_two.limit
212
+ expect(two_lock).to be_a Berater::Lock
183
213
 
184
- expect { limiter_one }.to be_incapacitated
185
- expect { limiter_two }.to be_incapacitated
214
+ expect(limiter_one).to be_incapacitated
215
+ expect(limiter_two).to be_incapacitated
216
+ end
217
+ end
218
+ end
219
+
220
+ describe '#overloaded?' do
221
+ let(:limiter) { described_class.new(:key, 1, timeout: 30) }
222
+
223
+ it 'works' do
224
+ expect(limiter.overloaded?).to be false
225
+ lock = limiter.limit
226
+ expect(limiter.overloaded?).to be true
227
+ lock.release
228
+ expect(limiter.overloaded?).to be false
229
+ end
230
+
231
+ it 'respects timeout' do
232
+ expect(limiter.overloaded?).to be false
233
+ lock = limiter.limit
234
+ expect(limiter.overloaded?).to be true
235
+ Timecop.freeze(30)
236
+ expect(limiter.overloaded?).to be false
237
+ end
238
+ end
239
+
240
+ describe '#to_s' do
241
+ def check(capacity, expected)
242
+ expect(
243
+ described_class.new(:key, capacity).to_s
244
+ ).to match(expected)
245
+ end
246
+
247
+ it 'works' do
248
+ check(1, /1 at a time/)
249
+ check(3, /3 at a time/)
186
250
  end
187
251
  end
188
252