berater 0.3.0 → 0.6.2

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