berater 0.1.3 → 0.5.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.
@@ -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