berater 0.1.3 → 0.5.0

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
@@ -0,0 +1,213 @@
1
+ describe Berater::TestMode, order: :defined do
2
+ let(:reset_test_mode) { true }
3
+
4
+ after do
5
+ Berater.test_mode = nil if reset_test_mode
6
+ end
7
+
8
+ context 'after test_mode.rb was required, but not used' do
9
+ let(:reset_test_mode) { false }
10
+
11
+ it 'has already been loaded by "berater/rspec", unfortunately' do
12
+ expect {
13
+ expect { Berater.test_mode }.to raise_error(NoMethodError)
14
+ }.to fail
15
+ end
16
+
17
+ it 'defaults to off' do
18
+ expect(Berater.test_mode).to be nil
19
+ end
20
+
21
+ it 'did not prepend .new yet' do
22
+ expect(Berater::Limiter.singleton_class.ancestors).not_to include(described_class)
23
+ end
24
+
25
+ it 'prepends when first turned on' do
26
+ Berater.test_mode = :pass
27
+
28
+ expect(Berater::Limiter.singleton_class.ancestors).to include(described_class)
29
+ end
30
+
31
+ it 'preserves the original functionality via super' do
32
+ expect { Berater::Limiter.new }.to raise_error(NotImplementedError)
33
+ end
34
+ end
35
+
36
+ describe '.test_mode' do
37
+ it 'can be turned on' do
38
+ Berater.test_mode = :pass
39
+ expect(Berater.test_mode).to be :pass
40
+
41
+ Berater.test_mode = :fail
42
+ expect(Berater.test_mode).to be :fail
43
+ end
44
+
45
+ it 'can be turned off' do
46
+ Berater.test_mode = nil
47
+ expect(Berater.test_mode).to be nil
48
+ end
49
+
50
+ it 'validates input' do
51
+ expect { Berater.test_mode = :foo }.to raise_error(ArgumentError)
52
+ end
53
+ end
54
+
55
+ shared_examples 'it always works, without redis' do
56
+ before do
57
+ Berater.redis = nil
58
+ expect_any_instance_of(Berater::LuaScript).not_to receive(:eval)
59
+ end
60
+
61
+ it_behaves_like 'it is not overloaded'
62
+
63
+ it 'always works' do
64
+ 10.times { subject.limit }
65
+ end
66
+ end
67
+
68
+ shared_examples 'it never works, without redis' do
69
+ before do
70
+ Berater.redis = nil
71
+ expect_any_instance_of(Berater::LuaScript).not_to receive(:eval)
72
+ end
73
+
74
+ it_behaves_like 'it is overloaded'
75
+
76
+ it 'never works' do
77
+ expect { subject }.to be_overloaded
78
+ end
79
+ end
80
+
81
+ describe 'Unlimiter' do
82
+ subject { Berater::Unlimiter.new }
83
+
84
+ context 'when test_mode = nil' do
85
+ before { Berater.test_mode = nil }
86
+
87
+ it { is_expected.to be_a Berater::Unlimiter }
88
+ it_behaves_like 'it always works, without redis'
89
+ end
90
+
91
+ context 'when test_mode = :pass' do
92
+ before { Berater.test_mode = :pass }
93
+
94
+ it { is_expected.to be_a Berater::Unlimiter }
95
+ it_behaves_like 'it always works, without redis'
96
+ end
97
+
98
+ context 'when test_mode = :fail' do
99
+ before { Berater.test_mode = :fail }
100
+
101
+ it { is_expected.to be_a Berater::Unlimiter }
102
+ it_behaves_like 'it never works, without redis'
103
+ end
104
+ end
105
+
106
+ describe 'Inhibitor' do
107
+ subject { Berater::Inhibitor.new }
108
+
109
+ context 'when test_mode = nil' do
110
+ before { Berater.test_mode = nil }
111
+
112
+ it { is_expected.to be_a Berater::Inhibitor }
113
+ it_behaves_like 'it never works, without redis'
114
+ end
115
+
116
+ context 'when test_mode = :pass' do
117
+ before { Berater.test_mode = :pass }
118
+
119
+ it { is_expected.to be_a Berater::Inhibitor }
120
+ it_behaves_like 'it always works, without redis'
121
+ end
122
+
123
+ context 'when test_mode = :fail' do
124
+ before { Berater.test_mode = :fail }
125
+
126
+ it { is_expected.to be_a Berater::Inhibitor }
127
+ it_behaves_like 'it never works, without redis'
128
+ end
129
+ end
130
+
131
+ describe 'RateLimiter' do
132
+ subject { Berater::RateLimiter.new(:key, 1, :second) }
133
+
134
+ shared_examples 'a RateLimiter' do
135
+ it { is_expected.to be_a Berater::RateLimiter }
136
+
137
+ it 'checks arguments' do
138
+ expect {
139
+ Berater::RateLimiter.new(:key, 1)
140
+ }.to raise_error(ArgumentError)
141
+ end
142
+ end
143
+
144
+ context 'when test_mode = nil' do
145
+ before { Berater.test_mode = nil }
146
+
147
+ it_behaves_like 'a RateLimiter'
148
+ it_behaves_like 'it is not overloaded'
149
+
150
+ it 'works per usual' do
151
+ expect(Berater::RateLimiter::LUA_SCRIPT).to receive(:eval).twice.and_call_original
152
+ expect(subject.limit).to be_a Berater::Lock
153
+ expect { subject.limit }.to be_overloaded
154
+ end
155
+ end
156
+
157
+ context 'when test_mode = :pass' do
158
+ before { Berater.test_mode = :pass }
159
+
160
+ it_behaves_like 'a RateLimiter'
161
+ it_behaves_like 'it always works, without redis'
162
+ end
163
+
164
+ context 'when test_mode = :fail' do
165
+ before { Berater.test_mode = :fail }
166
+
167
+ it_behaves_like 'a RateLimiter'
168
+ it_behaves_like 'it never works, without redis'
169
+ end
170
+ end
171
+
172
+ describe 'ConcurrencyLimiter' do
173
+ subject { Berater::ConcurrencyLimiter.new(:key, 1) }
174
+
175
+ shared_examples 'a ConcurrencyLimiter' do
176
+ it { expect(subject).to be_a Berater::ConcurrencyLimiter }
177
+
178
+ it 'checks arguments' do
179
+ expect {
180
+ Berater::ConcurrencyLimiter.new(:key, 1.0)
181
+ }.to raise_error(ArgumentError)
182
+ end
183
+ end
184
+
185
+ context 'when test_mode = nil' do
186
+ before { Berater.test_mode = nil }
187
+
188
+ it_behaves_like 'a ConcurrencyLimiter'
189
+ it_behaves_like 'it is not overloaded'
190
+
191
+ it 'works per usual' do
192
+ expect(Berater::ConcurrencyLimiter::LUA_SCRIPT).to receive(:eval).twice.and_call_original
193
+ expect(subject.limit).to be_a Berater::Lock
194
+ expect { subject.limit }.to be_overloaded
195
+ end
196
+ end
197
+
198
+ context 'when test_mode = :pass' do
199
+ before { Berater.test_mode = :pass }
200
+
201
+ it_behaves_like 'a ConcurrencyLimiter'
202
+ it_behaves_like 'it always works, without redis'
203
+ end
204
+
205
+ context 'when test_mode = :fail' do
206
+ before { Berater.test_mode = :fail }
207
+
208
+ it_behaves_like 'a ConcurrencyLimiter'
209
+ it_behaves_like 'it never works, without redis'
210
+ end
211
+ end
212
+
213
+ end
@@ -1,5 +1,5 @@
1
1
  describe Berater::Unlimiter do
2
- before { Berater.mode = :unlimited }
2
+ it_behaves_like 'a limiter', described_class.new
3
3
 
4
4
  describe '.new' do
5
5
  it 'initializes without any arguments or options' do
@@ -7,55 +7,24 @@ describe Berater::Unlimiter do
7
7
  end
8
8
 
9
9
  it 'initializes with any arguments and options' do
10
- expect(described_class.new(:abc, x: 123)).to be_a described_class
10
+ expect(described_class.new(:abc, :def, x: 123)).to be_a described_class
11
11
  end
12
12
 
13
13
  it 'has default values' do
14
- expect(described_class.new.key).to eq described_class.to_s
14
+ expect(described_class.new.key).to be :unlimiter
15
15
  expect(described_class.new.redis).to be Berater.redis
16
16
  end
17
17
  end
18
18
 
19
- describe '.limit' do
20
- it 'works' do
21
- expect(described_class.limit).to be_nil
22
- end
23
-
24
- it 'yields' do
25
- expect {|b| described_class.limit(&b) }.to yield_control
26
- end
27
-
28
- it 'is never overloaded' do
29
- 10.times do
30
- expect { described_class.limit }.not_to be_overloaded
31
- end
32
- end
33
-
34
- it 'works with any arguments or options' do
35
- expect(described_class.limit(:abc, x: 123)).to be_nil
36
- end
37
- end
38
-
39
19
  describe '#limit' do
40
- let(:limiter) { described_class.new }
20
+ subject { described_class.new }
41
21
 
42
- it 'works' do
43
- expect(limiter.limit).to be_nil
44
- end
45
-
46
- it 'yields' do
47
- expect {|b| limiter.limit(&b) }.to yield_control
48
- end
22
+ it_behaves_like 'it is not overloaded'
49
23
 
50
24
  it 'is never overloaded' do
51
25
  10.times do
52
- expect { limiter.limit }.not_to be_overloaded
26
+ expect { subject.limit }.not_to be_overloaded
53
27
  end
54
28
  end
55
-
56
- it 'works with any arguments or options' do
57
- expect(limiter.limit(x: 123)).to be_nil
58
- end
59
29
  end
60
-
61
30
  end
@@ -0,0 +1,78 @@
1
+ describe Berater::Utils do
2
+ using Berater::Utils
3
+
4
+ describe '.to_usec' do
5
+ def f(val)
6
+ (val * 10**6).to_i
7
+ end
8
+
9
+ it 'works with integers' do
10
+ expect(0.to_usec).to be f(0)
11
+ expect(3.to_usec).to be f(3)
12
+ end
13
+
14
+ it 'works with floats' do
15
+ expect(0.1.to_usec).to be f(0.1)
16
+ expect(3.0.to_usec).to be f(3)
17
+ end
18
+
19
+ it 'has great precision' do
20
+ expect(0.123456.to_usec).to be 123456
21
+ expect(123456.654321.to_usec).to be 123456654321
22
+ end
23
+
24
+ it 'works with symbols that are keywords' do
25
+ expect(:sec.to_usec).to be f(1)
26
+ expect(:second.to_usec).to be f(1)
27
+ expect(:seconds.to_usec).to be f(1)
28
+
29
+ expect(:min.to_usec).to be f(60)
30
+ expect(:minute.to_usec).to be f(60)
31
+ expect(:minutes.to_usec).to be f(60)
32
+
33
+ expect(:hour.to_usec).to be f(60 * 60)
34
+ expect(:hours.to_usec).to be f(60 * 60)
35
+ end
36
+
37
+ it 'works with strings that are keywords' do
38
+ expect('sec'.to_usec).to be f(1)
39
+ expect('second'.to_usec).to be f(1)
40
+ expect('seconds'.to_usec).to be f(1)
41
+
42
+ expect('min'.to_usec).to be f(60)
43
+ expect('minute'.to_usec).to be f(60)
44
+ expect('minutes'.to_usec).to be f(60)
45
+
46
+ expect('hour'.to_usec).to be f(60 * 60)
47
+ expect('hours'.to_usec).to be f(60 * 60)
48
+ end
49
+
50
+ it 'works with strings that are numeric' do
51
+ expect('0'.to_usec).to be f(0)
52
+ expect('3'.to_usec).to be f(3)
53
+
54
+ expect('0.1'.to_usec).to be f(0.1)
55
+ expect('3.0'.to_usec).to be f(3)
56
+ end
57
+
58
+ context 'with erroneous values' do
59
+ def e(val)
60
+ expect { val.to_usec }.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