berater 0.2.0 → 0.6.1

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,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