berater 0.2.0 → 0.6.1

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,225 @@
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(NotImplementedError)
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
70
+
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
79
+
80
+ describe 'Unlimiter' do
81
+ subject { Berater::Unlimiter.new }
82
+
83
+ context 'when test_mode = nil' do
84
+ before { Berater.test_mode = nil }
85
+
86
+ it { is_expected.to be_a Berater::Unlimiter }
87
+ it_behaves_like 'it always works, without redis'
88
+ end
89
+
90
+ context 'when test_mode = :pass' do
91
+ before { Berater.test_mode = :pass }
92
+
93
+ it { is_expected.to be_a Berater::Unlimiter }
94
+ it_behaves_like 'it always works, without redis'
95
+ end
96
+
97
+ context 'when test_mode = :fail' do
98
+ before { Berater.test_mode = :fail }
99
+
100
+ it { is_expected.to be_a Berater::Unlimiter }
101
+ it_behaves_like 'it never works, without redis'
102
+
103
+ it 'supports class specific logic' do
104
+ expect(subject.overloaded?).to be true
105
+ expect { subject.limit }.to raise_error(Berater::Overloaded)
106
+ end
107
+ end
108
+ end
109
+
110
+ describe 'Inhibitor' do
111
+ subject { Berater::Inhibitor.new }
112
+
113
+ context 'when test_mode = nil' do
114
+ before { Berater.test_mode = nil }
115
+
116
+ it { is_expected.to be_a Berater::Inhibitor }
117
+ it_behaves_like 'it never works, without redis'
118
+ end
119
+
120
+ context 'when test_mode = :pass' do
121
+ before { Berater.test_mode = :pass }
122
+
123
+ it { is_expected.to be_a Berater::Inhibitor }
124
+ it_behaves_like 'it always works, without redis'
125
+ end
126
+
127
+ context 'when test_mode = :fail' do
128
+ before { Berater.test_mode = :fail }
129
+
130
+ it { is_expected.to be_a Berater::Inhibitor }
131
+ it_behaves_like 'it never works, without redis'
132
+
133
+ it 'supports class specific logic' do
134
+ expect(subject.inhibited?).to be true
135
+ expect { subject.limit }.to raise_error(Berater::Inhibitor::Inhibited)
136
+ end
137
+ end
138
+ end
139
+
140
+ describe 'RateLimiter' do
141
+ subject { Berater::RateLimiter.new(:key, 1, :second) }
142
+
143
+ shared_examples 'a RateLimiter' do
144
+ it { is_expected.to be_a Berater::RateLimiter }
145
+
146
+ it 'checks arguments' do
147
+ expect {
148
+ Berater::RateLimiter.new(:key, 1)
149
+ }.to raise_error(ArgumentError)
150
+ end
151
+ end
152
+
153
+ context 'when test_mode = nil' do
154
+ before { Berater.test_mode = nil }
155
+
156
+ it_behaves_like 'a RateLimiter'
157
+ it_behaves_like 'it is not overloaded'
158
+
159
+ it 'works per usual' do
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
163
+ end
164
+ end
165
+
166
+ context 'when test_mode = :pass' do
167
+ before { Berater.test_mode = :pass }
168
+
169
+ it_behaves_like 'a RateLimiter'
170
+ it_behaves_like 'it always works, without redis'
171
+ end
172
+
173
+ context 'when test_mode = :fail' do
174
+ before { Berater.test_mode = :fail }
175
+
176
+ it_behaves_like 'a RateLimiter'
177
+ it_behaves_like 'it never works, without redis'
178
+
179
+ it 'supports class specific logic' do
180
+ expect(subject.overrated?).to be true
181
+ expect { subject.limit }.to raise_error(Berater::RateLimiter::Overrated)
182
+ end
183
+ end
184
+ end
185
+
186
+ describe 'ConcurrencyLimiter' do
187
+ subject { Berater::ConcurrencyLimiter.new(:key, 1) }
188
+
189
+ context 'when test_mode = nil' do
190
+ before { Berater.test_mode = nil }
191
+
192
+ it { is_expected.to be_a Berater::ConcurrencyLimiter }
193
+
194
+ it_behaves_like 'it is not overloaded'
195
+
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
200
+ end
201
+ end
202
+
203
+ context 'when test_mode = :pass' do
204
+ before { Berater.test_mode = :pass }
205
+
206
+ it { is_expected.to be_a Berater::ConcurrencyLimiter }
207
+
208
+ it_behaves_like 'it always works, without redis'
209
+ end
210
+
211
+ context 'when test_mode = :fail' do
212
+ before { Berater.test_mode = :fail }
213
+
214
+ it { is_expected.to be_a Berater::ConcurrencyLimiter }
215
+
216
+ it_behaves_like 'it never works, without redis'
217
+
218
+ it 'supports class specific logic' do
219
+ expect(subject.incapacitated?).to be true
220
+ expect { subject.limit }.to raise_error(Berater::ConcurrencyLimiter::Incapacitated)
221
+ end
222
+ end
223
+ end
224
+
225
+ end
@@ -1,4 +1,6 @@
1
1
  describe Berater::Unlimiter do
2
+ it_behaves_like 'a limiter', described_class.new
3
+
2
4
  describe '.new' do
3
5
  it 'initializes without any arguments or options' do
4
6
  expect(described_class.new).to be_a described_class
@@ -15,23 +17,14 @@ describe Berater::Unlimiter do
15
17
  end
16
18
 
17
19
  describe '#limit' do
18
- let(:limiter) { described_class.new }
19
-
20
- it 'works' do
21
- expect {|b| limiter.limit(&b) }.to yield_control
22
- end
20
+ subject { described_class.new }
23
21
 
24
- it 'works without a block' do
25
- expect(limiter.limit).to be_a Berater::Lock
26
- end
22
+ it_behaves_like 'it is not overloaded'
27
23
 
28
24
  it 'is never overloaded' do
29
25
  10.times do
30
- expect { limiter.limit }.not_to be_overloaded
26
+ expect { subject.limit }.not_to be_overloaded
31
27
  end
32
28
  end
33
29
  end
34
-
35
- it_behaves_like 'a lock', described_class.new
36
-
37
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
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: berater
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-16 00:00:00.000000000 Z
11
+ date: 2021-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: benchmark
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: byebug
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -108,28 +122,39 @@ dependencies:
108
122
  - - ">="
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
111
- description: rate limiter
125
+ description: work...within limits
112
126
  email:
113
127
  executables: []
114
128
  extensions: []
115
129
  extra_rdoc_files: []
116
130
  files:
117
131
  - lib/berater.rb
118
- - lib/berater/base_limiter.rb
119
132
  - lib/berater/concurrency_limiter.rb
133
+ - lib/berater/dsl.rb
120
134
  - lib/berater/inhibitor.rb
135
+ - lib/berater/limiter.rb
121
136
  - lib/berater/lock.rb
137
+ - lib/berater/lua_script.rb
122
138
  - lib/berater/rate_limiter.rb
139
+ - lib/berater/rspec.rb
140
+ - lib/berater/rspec/matchers.rb
141
+ - lib/berater/test_mode.rb
123
142
  - lib/berater/unlimiter.rb
143
+ - lib/berater/utils.rb
124
144
  - lib/berater/version.rb
125
145
  - spec/berater_spec.rb
126
146
  - spec/concurrency_limiter_spec.rb
127
- - spec/concurrency_lock_spec.rb
147
+ - spec/dsl_refinement_spec.rb
148
+ - spec/dsl_spec.rb
128
149
  - spec/inhibitor_spec.rb
129
- - spec/matcher_spec.rb
150
+ - spec/limiter_spec.rb
151
+ - spec/lua_script_spec.rb
152
+ - spec/matchers_spec.rb
130
153
  - spec/rate_limiter_spec.rb
131
- - spec/rate_lock_spec.rb
154
+ - spec/riddle_spec.rb
155
+ - spec/test_mode_spec.rb
132
156
  - spec/unlimiter_spec.rb
157
+ - spec/utils_spec.rb
133
158
  homepage: https://github.com/dpep/berater_rb
134
159
  licenses:
135
160
  - MIT
@@ -155,10 +180,15 @@ specification_version: 4
155
180
  summary: Berater
156
181
  test_files:
157
182
  - spec/rate_limiter_spec.rb
158
- - spec/matcher_spec.rb
159
- - spec/rate_lock_spec.rb
183
+ - spec/matchers_spec.rb
184
+ - spec/dsl_refinement_spec.rb
185
+ - spec/test_mode_spec.rb
186
+ - spec/dsl_spec.rb
187
+ - spec/lua_script_spec.rb
160
188
  - spec/concurrency_limiter_spec.rb
161
- - spec/concurrency_lock_spec.rb
189
+ - spec/riddle_spec.rb
190
+ - spec/utils_spec.rb
162
191
  - spec/berater_spec.rb
192
+ - spec/limiter_spec.rb
163
193
  - spec/inhibitor_spec.rb
164
194
  - spec/unlimiter_spec.rb