berater 0.3.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,102 @@
1
+ # Can you build a rate limiter from a concurrency limiter? Yes!
2
+
3
+ class RateRiddler
4
+ def self.limit(capacity, interval)
5
+ lock = Berater::ConcurrencyLimiter.new(:key, capacity, timeout: interval).limit
6
+ yield if block_given?
7
+ # allow lock to time out rather than be released
8
+ end
9
+ end
10
+
11
+
12
+ describe 'a ConcurrencyLimiter-derived rate limiter' do
13
+ def limit(&block)
14
+ RateRiddler.limit(1, :second, &block)
15
+ end
16
+
17
+ it 'works' do
18
+ expect(limit { 123 }).to eq 123
19
+ end
20
+
21
+ it 'respects limits' do
22
+ limit
23
+ expect { limit }.to be_overloaded
24
+ end
25
+
26
+ it 'resets over time' do
27
+ limit
28
+ expect { limit }.to be_overloaded
29
+
30
+ Timecop.freeze(1)
31
+
32
+ limit
33
+ expect { limit }.to be_overloaded
34
+ end
35
+ end
36
+
37
+
38
+ # Can you build a concurrency limiter from a rate limiter? Almost...
39
+
40
+ class ConcurrenyRiddler
41
+ def self.limit(capacity, timeout: nil)
42
+ timeout ||= 1_000 # fake infinity
43
+
44
+ limiter = Berater::RateLimiter.new(:key, capacity, timeout)
45
+ limiter.limit
46
+ yield if block_given?
47
+ ensure
48
+ # decrement counter
49
+ limiter.redis.decr(limiter.send(:cache_key, :key))
50
+ end
51
+ end
52
+
53
+
54
+ describe 'a RateLimiter-derived concurrency limiter' do
55
+ def limit(capacity = 1, timeout: nil, &block)
56
+ ConcurrenyRiddler.limit(capacity, timeout: timeout, &block)
57
+ end
58
+
59
+ it 'works' do
60
+ expect(limit { 123 }).to eq 123
61
+ end
62
+
63
+ it 'respects limits' do
64
+ limit do
65
+ # a second, simultaneous request isn't allowed
66
+ expect { limit }.to be_overloaded
67
+ end
68
+
69
+ # but now it'll work
70
+ limit
71
+ end
72
+
73
+ it 'resets over time' do
74
+ limit(timeout: 1) do
75
+ expect { limit }.to be_overloaded
76
+
77
+ # ...wait for it
78
+ Timecop.freeze(10)
79
+
80
+ limit(timeout: 1)
81
+ end
82
+ end
83
+
84
+ it "has no memory of the order, so timeouts don't work quite right" do
85
+ limit(2, timeout: 1) do
86
+ Timecop.freeze(0.5)
87
+
88
+ limit(2, timeout: 1) do
89
+ # this is where the masquerading breaks. the first lock is still
90
+ # being held and within it's timeout limit, however the RaterLimiter
91
+ # decremented the count internally since enough time has passed.
92
+ # This next call *should* fail, but doesn't.
93
+
94
+ expect {
95
+ expect { limit(2, timeout: 1) }.to be_overloaded
96
+ }.to fail
97
+
98
+ # ...close, but not quite!
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,84 +1,147 @@
1
- require 'berater/test_mode'
1
+ describe Berater::TestMode do
2
+ after do
3
+ Berater.test_mode = nil
4
+ end
5
+
6
+ context 'after test_mode.rb has been loaded' do
7
+ it 'monkey patches Berater' do
8
+ expect(Berater).to respond_to(:test_mode)
9
+ end
10
+
11
+ it 'defaults to off' do
12
+ expect(Berater.test_mode).to be nil
13
+ end
14
+
15
+ it 'prepends Limiter subclasses' do
16
+ expect(Berater::Unlimiter.ancestors).to include(described_class)
17
+ expect(Berater::Inhibitor.ancestors).to include(described_class)
18
+ end
19
+
20
+ it 'preserves the original functionality via super' do
21
+ expect { Berater::Limiter.new }.to raise_error(NoMethodError)
22
+ end
23
+ end
24
+
25
+ describe '.test_mode' do
26
+ it 'can be turned on' do
27
+ Berater.test_mode = :pass
28
+ expect(Berater.test_mode).to be :pass
29
+
30
+ Berater.test_mode = :fail
31
+ expect(Berater.test_mode).to be :fail
32
+ end
33
+
34
+ it 'can be turned off' do
35
+ Berater.test_mode = nil
36
+ expect(Berater.test_mode).to be nil
37
+ end
38
+
39
+ it 'validates input' do
40
+ expect { Berater.test_mode = :foo }.to raise_error(ArgumentError)
41
+ end
42
+
43
+ it 'works no matter when limiter was created' do
44
+ limiter = Berater::Unlimiter.new
45
+ expect(limiter).not_to be_overloaded
46
+
47
+ Berater.test_mode = :fail
48
+ expect(limiter).to be_overloaded
49
+ end
50
+
51
+ it 'supports a generic expectation' do
52
+ Berater.test_mode = :pass
53
+ expect_any_instance_of(Berater::Limiter).to receive(:limit)
54
+ Berater::Unlimiter.new.limit
55
+ end
56
+ end
57
+
58
+ shared_examples 'it always works, without redis' do
59
+ before do
60
+ Berater.redis = nil
61
+ expect_any_instance_of(Berater::LuaScript).not_to receive(:eval)
62
+ end
63
+
64
+ it_behaves_like 'it is not overloaded'
65
+
66
+ it 'always works' do
67
+ 10.times { subject.limit }
68
+ end
69
+ end
2
70
 
3
- describe 'Berater.test_mode' do
4
- after { Berater.test_mode = nil }
71
+ shared_examples 'it never works, without redis' do
72
+ before do
73
+ Berater.redis = nil
74
+ expect_any_instance_of(Berater::LuaScript).not_to receive(:eval)
75
+ end
76
+
77
+ it_behaves_like 'it is overloaded'
78
+ end
5
79
 
6
80
  describe 'Unlimiter' do
7
- let(:limiter) { Berater::Unlimiter.new }
81
+ subject { Berater::Unlimiter.new }
8
82
 
9
83
  context 'when test_mode = nil' do
10
84
  before { Berater.test_mode = nil }
11
85
 
12
- it { expect(limiter).to be_a Berater::Unlimiter }
13
-
14
- it 'works per usual' do
15
- expect {|block| limiter.limit(&block) }.to yield_control
16
- 10.times { expect(limiter.limit).to be_a Berater::Lock }
17
- end
86
+ it { is_expected.to be_a Berater::Unlimiter }
87
+ it_behaves_like 'it always works, without redis'
18
88
  end
19
89
 
20
90
  context 'when test_mode = :pass' do
21
91
  before { Berater.test_mode = :pass }
22
92
 
23
- it { expect(limiter).to be_a Berater::Unlimiter }
24
-
25
- it 'always works' do
26
- expect {|block| limiter.limit(&block) }.to yield_control
27
- 10.times { expect(limiter.limit).to be_a Berater::Lock }
28
- end
93
+ it { is_expected.to be_a Berater::Unlimiter }
94
+ it_behaves_like 'it always works, without redis'
29
95
  end
30
96
 
31
97
  context 'when test_mode = :fail' do
32
98
  before { Berater.test_mode = :fail }
33
99
 
34
- it { expect(limiter).to be_a Berater::Unlimiter }
100
+ it { is_expected.to be_a Berater::Unlimiter }
101
+ it_behaves_like 'it never works, without redis'
35
102
 
36
- it 'never works' do
37
- expect { limiter }.to be_overloaded
103
+ it 'supports class specific logic' do
104
+ expect(subject.overloaded?).to be true
105
+ expect { subject.limit }.to raise_error(Berater::Overloaded)
38
106
  end
39
107
  end
40
108
  end
41
109
 
42
110
  describe 'Inhibitor' do
43
- let(:limiter) { Berater::Inhibitor.new }
111
+ subject { Berater::Inhibitor.new }
44
112
 
45
113
  context 'when test_mode = nil' do
46
114
  before { Berater.test_mode = nil }
47
115
 
48
- it { expect(limiter).to be_a Berater::Inhibitor }
49
-
50
- it 'works per usual' do
51
- expect { limiter }.to be_overloaded
52
- end
116
+ it { is_expected.to be_a Berater::Inhibitor }
117
+ it_behaves_like 'it never works, without redis'
53
118
  end
54
119
 
55
120
  context 'when test_mode = :pass' do
56
121
  before { Berater.test_mode = :pass }
57
122
 
58
- it { expect(limiter).to be_a Berater::Inhibitor }
59
-
60
- it 'always works' do
61
- expect {|block| limiter.limit(&block) }.to yield_control
62
- 10.times { expect(limiter.limit).to be_a Berater::Lock }
63
- end
123
+ it { is_expected.to be_a Berater::Inhibitor }
124
+ it_behaves_like 'it always works, without redis'
64
125
  end
65
126
 
66
127
  context 'when test_mode = :fail' do
67
128
  before { Berater.test_mode = :fail }
68
129
 
69
- it { expect(limiter).to be_a Berater::Inhibitor }
130
+ it { is_expected.to be_a Berater::Inhibitor }
131
+ it_behaves_like 'it never works, without redis'
70
132
 
71
- it 'never works' do
72
- expect { limiter }.to be_overloaded
133
+ it 'supports class specific logic' do
134
+ expect(subject.inhibited?).to be true
135
+ expect { subject.limit }.to raise_error(Berater::Inhibitor::Inhibited)
73
136
  end
74
137
  end
75
138
  end
76
139
 
77
140
  describe 'RateLimiter' do
78
- let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
141
+ subject { Berater::RateLimiter.new(:key, 1, :second) }
79
142
 
80
143
  shared_examples 'a RateLimiter' do
81
- it { expect(limiter).to be_a Berater::RateLimiter }
144
+ it { is_expected.to be_a Berater::RateLimiter }
82
145
 
83
146
  it 'checks arguments' do
84
147
  expect {
@@ -91,15 +154,12 @@ describe 'Berater.test_mode' do
91
154
  before { Berater.test_mode = nil }
92
155
 
93
156
  it_behaves_like 'a RateLimiter'
157
+ it_behaves_like 'it is not overloaded'
94
158
 
95
159
  it 'works per usual' do
96
- expect(limiter.redis).to receive(:multi).twice.and_call_original
97
- expect(limiter.limit).to be_a Berater::Lock
98
- expect { limiter.limit }.to be_overloaded
99
- end
100
-
101
- it 'yields per usual' do
102
- expect {|block| limiter.limit(&block) }.to yield_control
160
+ expect(Berater::RateLimiter::LUA_SCRIPT).to receive(:eval).twice.and_call_original
161
+ expect(subject.limit).to be_a Berater::Lock
162
+ expect { subject.limit }.to be_overloaded
103
163
  end
104
164
  end
105
165
 
@@ -107,75 +167,57 @@ describe 'Berater.test_mode' do
107
167
  before { Berater.test_mode = :pass }
108
168
 
109
169
  it_behaves_like 'a RateLimiter'
110
-
111
- it 'always works and without calling redis' do
112
- expect(limiter.redis).not_to receive(:multi)
113
- expect {|block| limiter.limit(&block) }.to yield_control
114
- 10.times { expect(limiter.limit).to be_a Berater::Lock }
115
- end
170
+ it_behaves_like 'it always works, without redis'
116
171
  end
117
172
 
118
173
  context 'when test_mode = :fail' do
119
174
  before { Berater.test_mode = :fail }
120
175
 
121
176
  it_behaves_like 'a RateLimiter'
177
+ it_behaves_like 'it never works, without redis'
122
178
 
123
- it 'never works and without calling redis' do
124
- expect(limiter.redis).not_to receive(:multi)
125
- expect { limiter }.to be_overloaded
179
+ it 'supports class specific logic' do
180
+ expect(subject.overrated?).to be true
181
+ expect { subject.limit }.to raise_error(Berater::RateLimiter::Overrated)
126
182
  end
127
183
  end
128
184
  end
129
185
 
130
186
  describe 'ConcurrencyLimiter' do
131
- let(:limiter) { Berater::ConcurrencyLimiter.new(:key, 1) }
132
-
133
- shared_examples 'a ConcurrencyLimiter' do
134
- it { expect(limiter).to be_a Berater::ConcurrencyLimiter }
135
-
136
- it 'checks arguments' do
137
- expect {
138
- Berater::ConcurrencyLimiter.new(:key, 1.0)
139
- }.to raise_error(ArgumentError)
140
- end
141
- end
187
+ subject { Berater::ConcurrencyLimiter.new(:key, 1) }
142
188
 
143
189
  context 'when test_mode = nil' do
144
190
  before { Berater.test_mode = nil }
145
191
 
146
- it_behaves_like 'a ConcurrencyLimiter'
192
+ it { is_expected.to be_a Berater::ConcurrencyLimiter }
147
193
 
148
- it 'works per usual' do
149
- expect(limiter.redis).to receive(:eval).twice.and_call_original
150
- expect(limiter.limit).to be_a Berater::Lock
151
- expect { limiter.limit }.to be_overloaded
152
- end
194
+ it_behaves_like 'it is not overloaded'
153
195
 
154
- it 'yields per usual' do
155
- expect {|block| limiter.limit(&block) }.to yield_control
196
+ it 'works per usual' do
197
+ expect(Berater::ConcurrencyLimiter::LUA_SCRIPT).to receive(:eval).twice.and_call_original
198
+ expect(subject.limit).to be_a Berater::Lock
199
+ expect { subject.limit }.to be_overloaded
156
200
  end
157
201
  end
158
202
 
159
203
  context 'when test_mode = :pass' do
160
204
  before { Berater.test_mode = :pass }
161
205
 
162
- it_behaves_like 'a ConcurrencyLimiter'
206
+ it { is_expected.to be_a Berater::ConcurrencyLimiter }
163
207
 
164
- it 'always works and without calling redis' do
165
- expect(limiter.redis).not_to receive(:eval)
166
- expect {|block| limiter.limit(&block) }.to yield_control
167
- 10.times { expect(limiter.limit).to be_a Berater::Lock }
168
- end
208
+ it_behaves_like 'it always works, without redis'
169
209
  end
170
210
 
171
211
  context 'when test_mode = :fail' do
172
212
  before { Berater.test_mode = :fail }
173
213
 
174
- it_behaves_like 'a ConcurrencyLimiter'
214
+ it { is_expected.to be_a Berater::ConcurrencyLimiter }
215
+
216
+ it_behaves_like 'it never works, without redis'
175
217
 
176
- it 'never works and without calling redis' do
177
- expect(limiter.redis).not_to receive(:eval)
178
- expect { limiter }.to be_overloaded
218
+ it 'supports class specific logic' do
219
+ expect(subject.incapacitated?).to be true
220
+ expect { subject.limit }.to raise_error(Berater::ConcurrencyLimiter::Incapacitated)
179
221
  end
180
222
  end
181
223
  end
@@ -17,19 +17,13 @@ describe Berater::Unlimiter do
17
17
  end
18
18
 
19
19
  describe '#limit' do
20
- let(:limiter) { described_class.new }
20
+ subject { described_class.new }
21
21
 
22
- it 'works' do
23
- expect {|b| limiter.limit(&b) }.to yield_control
24
- end
25
-
26
- it 'works without a block' do
27
- expect(limiter.limit).to be_a Berater::Lock
28
- end
22
+ it_behaves_like 'it is not overloaded'
29
23
 
30
24
  it 'is never overloaded' do
31
25
  10.times do
32
- expect { limiter.limit }.not_to be_overloaded
26
+ expect { subject.limit }.not_to be_overloaded
33
27
  end
34
28
  end
35
29
  end
@@ -0,0 +1,78 @@
1
+ describe Berater::Utils do
2
+ using Berater::Utils
3
+
4
+ describe '.to_msec' do
5
+ def f(val)
6
+ (val * 10**3).to_i
7
+ end
8
+
9
+ it 'works with integers' do
10
+ expect(0.to_msec).to be f(0)
11
+ expect(3.to_msec).to be f(3)
12
+ end
13
+
14
+ it 'works with floats' do
15
+ expect(0.1.to_msec).to be f(0.1)
16
+ expect(3.0.to_msec).to be f(3)
17
+ end
18
+
19
+ it 'truncates excessive precision' do
20
+ expect(0.123456.to_msec).to be 123
21
+ expect(123456.654321.to_msec).to be 123456654
22
+ end
23
+
24
+ it 'works with symbols that are keywords' do
25
+ expect(:sec.to_msec).to be f(1)
26
+ expect(:second.to_msec).to be f(1)
27
+ expect(:seconds.to_msec).to be f(1)
28
+
29
+ expect(:min.to_msec).to be f(60)
30
+ expect(:minute.to_msec).to be f(60)
31
+ expect(:minutes.to_msec).to be f(60)
32
+
33
+ expect(:hour.to_msec).to be f(60 * 60)
34
+ expect(:hours.to_msec).to be f(60 * 60)
35
+ end
36
+
37
+ it 'works with strings that are keywords' do
38
+ expect('sec'.to_msec).to be f(1)
39
+ expect('second'.to_msec).to be f(1)
40
+ expect('seconds'.to_msec).to be f(1)
41
+
42
+ expect('min'.to_msec).to be f(60)
43
+ expect('minute'.to_msec).to be f(60)
44
+ expect('minutes'.to_msec).to be f(60)
45
+
46
+ expect('hour'.to_msec).to be f(60 * 60)
47
+ expect('hours'.to_msec).to be f(60 * 60)
48
+ end
49
+
50
+ it 'works with strings that are numeric' do
51
+ expect('0'.to_msec).to be f(0)
52
+ expect('3'.to_msec).to be f(3)
53
+
54
+ expect('0.1'.to_msec).to be f(0.1)
55
+ expect('3.0'.to_msec).to be f(3)
56
+ end
57
+
58
+ context 'with erroneous values' do
59
+ def e(val)
60
+ expect { val.to_msec }.to raise_error(ArgumentError)
61
+ end
62
+
63
+ it 'rejects negative numbers' do
64
+ e(-1)
65
+ e(-1.2)
66
+ e('-1')
67
+ end
68
+
69
+ it 'rejects bogus symbols and strings' do
70
+ e('abc')
71
+ e('1a')
72
+ e(:abc)
73
+ e(Float::INFINITY)
74
+ end
75
+ end
76
+ end
77
+
78
+ end