berater 0.1.4 → 0.6.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.
@@ -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,206 @@
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
+ context 'when test_mode = nil' do
176
+ before { Berater.test_mode = nil }
177
+
178
+ it { is_expected.to be_a Berater::ConcurrencyLimiter }
179
+
180
+ it_behaves_like 'it is not overloaded'
181
+
182
+ it 'works per usual' do
183
+ expect(Berater::ConcurrencyLimiter::LUA_SCRIPT).to receive(:eval).twice.and_call_original
184
+ expect(subject.limit).to be_a Berater::Lock
185
+ expect { subject.limit }.to be_overloaded
186
+ end
187
+ end
188
+
189
+ context 'when test_mode = :pass' do
190
+ before { Berater.test_mode = :pass }
191
+
192
+ it { is_expected.to be_a Berater::ConcurrencyLimiter }
193
+
194
+ it_behaves_like 'it always works, without redis'
195
+ end
196
+
197
+ context 'when test_mode = :fail' do
198
+ before { Berater.test_mode = :fail }
199
+
200
+ it { is_expected.to be_a Berater::ConcurrencyLimiter }
201
+
202
+ it_behaves_like 'it never works, without redis'
203
+ end
204
+ end
205
+
206
+ 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_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