berater 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/lib/berater.rb +21 -3
- data/lib/berater/concurrency_limiter.rb +5 -1
- data/lib/berater/dsl.rb +57 -0
- data/lib/berater/inhibitor.rb +1 -1
- data/lib/berater/{base_limiter.rb → limiter.rb} +5 -1
- data/lib/berater/lock.rb +3 -3
- data/lib/berater/rate_limiter.rb +24 -8
- data/lib/berater/rspec.rb +12 -0
- data/lib/berater/rspec/matchers.rb +60 -0
- data/lib/berater/test_mode.rb +43 -0
- data/lib/berater/unlimiter.rb +2 -3
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +46 -0
- data/spec/concurrency_limiter_spec.rb +17 -13
- data/spec/{matcher_spec.rb → matchers_spec.rb} +0 -0
- data/spec/rate_limiter_spec.rb +60 -12
- data/spec/test_mode_spec.rb +183 -0
- data/spec/unlimiter_spec.rb +2 -3
- metadata +11 -9
- data/spec/concurrency_lock_spec.rb +0 -39
- data/spec/rate_lock_spec.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d007dbb664fa57c921047a28dd1e5a591cf5c51fba4931dbc367ab552e4b8a8d
|
4
|
+
data.tar.gz: c710d31a2d8cfdc50af4c2fae87cabad177096d13da52a7f9c48b61aa31b6038
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d9c211bb65e7341b98e637d3f0ce0e31f0a879437c1c309fb01e1efab5e5c6b2d5044158931ab16a67cbbdebd00ddb5f10afc97430230b153d8cc236f7517ab
|
7
|
+
data.tar.gz: 5d8b38bfd7683df641fd6891cd1a493079453ea6c61b2880581eeb2d963fb171365fb3448443e2548b34297d8dcfa352e67cff5688cd24d25422efb5e55d4afd
|
data/lib/berater.rb
CHANGED
@@ -15,7 +15,23 @@ module Berater
|
|
15
15
|
yield self
|
16
16
|
end
|
17
17
|
|
18
|
-
def new(key, mode, *args, **opts)
|
18
|
+
def new(key, mode = nil, *args, **opts, &block)
|
19
|
+
if mode.nil?
|
20
|
+
unless args.empty?
|
21
|
+
raise ArgumentError, '0 arguments expected with block'
|
22
|
+
end
|
23
|
+
|
24
|
+
unless block_given?
|
25
|
+
raise ArgumentError, 'expected either mode or block'
|
26
|
+
end
|
27
|
+
|
28
|
+
mode, *args = DSL.eval(&block)
|
29
|
+
else
|
30
|
+
if block_given?
|
31
|
+
raise ArgumentError, 'expected either mode or block, not both'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
19
35
|
klass = MODES[mode.to_sym]
|
20
36
|
|
21
37
|
unless klass
|
@@ -42,8 +58,8 @@ def Berater(key, mode, *args, **opts, &block)
|
|
42
58
|
Berater.new(key, mode, *args, **opts).limit(&block)
|
43
59
|
end
|
44
60
|
|
45
|
-
# load
|
46
|
-
require 'berater/
|
61
|
+
# load limiters
|
62
|
+
require 'berater/limiter'
|
47
63
|
require 'berater/concurrency_limiter'
|
48
64
|
require 'berater/inhibitor'
|
49
65
|
require 'berater/rate_limiter'
|
@@ -53,3 +69,5 @@ Berater.register(:concurrency, Berater::ConcurrencyLimiter)
|
|
53
69
|
Berater.register(:inhibited, Berater::Inhibitor)
|
54
70
|
Berater.register(:rate, Berater::RateLimiter)
|
55
71
|
Berater.register(:unlimited, Berater::Unlimiter)
|
72
|
+
|
73
|
+
require 'berater/dsl'
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Berater
|
2
|
-
class ConcurrencyLimiter <
|
2
|
+
class ConcurrencyLimiter < Limiter
|
3
3
|
|
4
4
|
class Incapacitated < Overloaded; end
|
5
5
|
|
@@ -85,5 +85,9 @@ module Berater
|
|
85
85
|
res == true || res == 1 # depending on which version of Redis
|
86
86
|
end
|
87
87
|
|
88
|
+
def to_s
|
89
|
+
"#<#{self.class}(#{key}: #{capacity} at a time)>"
|
90
|
+
end
|
91
|
+
|
88
92
|
end
|
89
93
|
end
|
data/lib/berater/dsl.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Berater
|
2
|
+
module DSL
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def eval &block
|
6
|
+
@keywords ||= Class.new do
|
7
|
+
# create a class where DSL keywords are methods
|
8
|
+
KEYWORDS.each do |keyword|
|
9
|
+
define_singleton_method(keyword) { keyword }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
install
|
14
|
+
@keywords.class_eval &block
|
15
|
+
ensure
|
16
|
+
uninstall
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def each &block
|
22
|
+
Berater::MODES.map do |mode, limiter|
|
23
|
+
next unless limiter.const_defined?(:DSL, false)
|
24
|
+
limiter.const_get(:DSL)
|
25
|
+
end.compact.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
KEYWORDS = [
|
29
|
+
:second, :minute, :hour,
|
30
|
+
:unlimited, :inhibited,
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
def install
|
34
|
+
Integer.class_eval do
|
35
|
+
def per(unit)
|
36
|
+
[ :rate, self, unit ]
|
37
|
+
end
|
38
|
+
alias every per
|
39
|
+
|
40
|
+
def at_once
|
41
|
+
[ :concurrency, self ]
|
42
|
+
end
|
43
|
+
alias concurrently at_once
|
44
|
+
alias at_a_time at_once
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def uninstall
|
49
|
+
Integer.remove_method :per
|
50
|
+
Integer.remove_method :every
|
51
|
+
|
52
|
+
Integer.remove_method :at_once
|
53
|
+
Integer.remove_method :concurrently
|
54
|
+
Integer.remove_method :at_a_time
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/berater/inhibitor.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Berater
|
2
|
-
class
|
2
|
+
class Limiter
|
3
3
|
|
4
4
|
attr_reader :key, :options
|
5
5
|
|
@@ -11,6 +11,10 @@ module Berater
|
|
11
11
|
raise NotImplementedError
|
12
12
|
end
|
13
13
|
|
14
|
+
def to_s
|
15
|
+
"#<#{self.class}>"
|
16
|
+
end
|
17
|
+
|
14
18
|
protected
|
15
19
|
|
16
20
|
def initialize(key, **opts)
|
data/lib/berater/lock.rb
CHANGED
@@ -17,7 +17,7 @@ module Berater
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def expired?
|
20
|
-
timeout
|
20
|
+
timeout ? @locked_at + timeout < Time.now : false
|
21
21
|
end
|
22
22
|
|
23
23
|
def release
|
@@ -28,8 +28,8 @@ module Berater
|
|
28
28
|
@release_fn ? @release_fn.call : true
|
29
29
|
end
|
30
30
|
|
31
|
-
|
32
|
-
limiter.respond_to?(:timeout) ? limiter.timeout :
|
31
|
+
def timeout
|
32
|
+
limiter.respond_to?(:timeout) ? limiter.timeout : nil
|
33
33
|
end
|
34
34
|
|
35
35
|
end
|
data/lib/berater/rate_limiter.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Berater
|
2
|
-
class RateLimiter <
|
2
|
+
class RateLimiter < Limiter
|
3
3
|
|
4
4
|
class Overrated < Overloaded; end
|
5
5
|
|
@@ -28,6 +28,7 @@ module Berater
|
|
28
28
|
case @interval
|
29
29
|
when Integer
|
30
30
|
raise ArgumentError, "interval must be >= 0" unless @interval >= 0
|
31
|
+
@interval_sec = @interval
|
31
32
|
when String
|
32
33
|
@interval = @interval.to_sym
|
33
34
|
when Symbol
|
@@ -38,28 +39,29 @@ module Berater
|
|
38
39
|
if @interval.is_a? Symbol
|
39
40
|
case @interval
|
40
41
|
when :sec, :second, :seconds
|
41
|
-
@interval =
|
42
|
+
@interval = :second
|
43
|
+
@interval_sec = 1
|
42
44
|
when :min, :minute, :minutes
|
43
|
-
@interval =
|
45
|
+
@interval = :minute
|
46
|
+
@interval_sec = 60
|
44
47
|
when :hour, :hours
|
45
|
-
@interval =
|
48
|
+
@interval = :hour
|
49
|
+
@interval_sec = 60 * 60
|
46
50
|
else
|
47
51
|
raise ArgumentError, "unexpected interval value: #{interval}"
|
48
52
|
end
|
49
53
|
end
|
50
|
-
|
51
|
-
@interval
|
52
54
|
end
|
53
55
|
|
54
56
|
def limit
|
55
57
|
ts = Time.now.to_i
|
56
58
|
|
57
59
|
# bucket into time slot
|
58
|
-
rkey = "%s:%d" % [ cache_key(key), ts - ts % @
|
60
|
+
rkey = "%s:%d" % [ cache_key(key), ts - ts % @interval_sec ]
|
59
61
|
|
60
62
|
count, _ = redis.multi do
|
61
63
|
redis.incr rkey
|
62
|
-
redis.expire rkey, @
|
64
|
+
redis.expire rkey, @interval_sec * 2
|
63
65
|
end
|
64
66
|
|
65
67
|
raise Overrated if count > @count
|
@@ -77,6 +79,20 @@ module Berater
|
|
77
79
|
end
|
78
80
|
end
|
79
81
|
|
82
|
+
def to_s
|
83
|
+
msg = if @interval.is_a? Integer
|
84
|
+
if @interval == 1
|
85
|
+
"every second"
|
86
|
+
else
|
87
|
+
"every #{@interval} seconds"
|
88
|
+
end
|
89
|
+
else
|
90
|
+
"per #{@interval}"
|
91
|
+
end
|
92
|
+
|
93
|
+
"#<#{self.class}(#{key}: #{count} #{msg})>"
|
94
|
+
end
|
95
|
+
|
80
96
|
end
|
81
97
|
end
|
82
98
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module BeraterMatchers
|
2
|
+
class Overloaded
|
3
|
+
def initialize(type)
|
4
|
+
@type = type
|
5
|
+
end
|
6
|
+
|
7
|
+
def supports_block_expectations?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
def matches?(obj)
|
12
|
+
begin
|
13
|
+
case obj
|
14
|
+
when Proc
|
15
|
+
# eg. expect { ... }.to be_overrated
|
16
|
+
res = obj.call
|
17
|
+
|
18
|
+
if res.is_a? Berater::Limiter
|
19
|
+
# eg. expect { Berater.new(...) }.to be_overrated
|
20
|
+
res.limit {}
|
21
|
+
end
|
22
|
+
when Berater::Limiter
|
23
|
+
# eg. expect(Berater.new(...)).to be_overrated
|
24
|
+
obj.limit {}
|
25
|
+
end
|
26
|
+
|
27
|
+
false
|
28
|
+
rescue @type
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# def description
|
34
|
+
# it { expect { Berater.new(:inhibitor) }.not_to be_overrated }
|
35
|
+
|
36
|
+
def failure_message
|
37
|
+
"expected #{@type} to be raised"
|
38
|
+
end
|
39
|
+
|
40
|
+
def failure_message_when_negated
|
41
|
+
"did not expect #{@type} to be raised"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def be_overloaded
|
46
|
+
Overloaded.new(Berater::Overloaded)
|
47
|
+
end
|
48
|
+
|
49
|
+
def be_overrated
|
50
|
+
Overloaded.new(Berater::RateLimiter::Overrated)
|
51
|
+
end
|
52
|
+
|
53
|
+
def be_incapacitated
|
54
|
+
Overloaded.new(Berater::ConcurrencyLimiter::Incapacitated)
|
55
|
+
end
|
56
|
+
|
57
|
+
def be_inhibited
|
58
|
+
Overloaded.new(Berater::Inhibitor::Inhibited)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'berater'
|
2
|
+
|
3
|
+
module Berater
|
4
|
+
extend self
|
5
|
+
|
6
|
+
attr_reader :test_mode
|
7
|
+
|
8
|
+
def test_mode=(mode)
|
9
|
+
unless [ nil, :pass, :fail ].include?(mode)
|
10
|
+
raise ArgumentError, "invalid mode: #{Berater.test_mode}"
|
11
|
+
end
|
12
|
+
|
13
|
+
@test_mode = mode
|
14
|
+
end
|
15
|
+
|
16
|
+
class Limiter
|
17
|
+
def self.new(*args, **opts)
|
18
|
+
return super unless Berater.test_mode
|
19
|
+
|
20
|
+
# chose a stub class with desired behavior
|
21
|
+
stub_klass = case Berater.test_mode
|
22
|
+
when :pass
|
23
|
+
Berater::Unlimiter
|
24
|
+
when :fail
|
25
|
+
Berater::Inhibitor
|
26
|
+
end
|
27
|
+
|
28
|
+
# don't stub self
|
29
|
+
return super if self < stub_klass
|
30
|
+
|
31
|
+
# swap out limit method with stub
|
32
|
+
super.tap do |instance|
|
33
|
+
stub = stub_klass.allocate
|
34
|
+
stub.send(:initialize, *args, **opts)
|
35
|
+
|
36
|
+
instance.define_singleton_method(:limit) do |&block|
|
37
|
+
stub.limit(&block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/lib/berater/unlimiter.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
module Berater
|
2
|
-
class Unlimiter <
|
2
|
+
class Unlimiter < Limiter
|
3
3
|
|
4
4
|
def initialize(key = :unlimiter, *args, **opts)
|
5
5
|
super(key, **opts)
|
6
6
|
end
|
7
7
|
|
8
8
|
def limit
|
9
|
-
|
10
|
-
lock = Lock.new(self, count, count)
|
9
|
+
lock = Lock.new(self, 0, 0)
|
11
10
|
|
12
11
|
if block_given?
|
13
12
|
begin
|
data/lib/berater/version.rb
CHANGED
data/spec/berater_spec.rb
CHANGED
@@ -120,6 +120,52 @@ describe Berater do
|
|
120
120
|
expect {|b| Berater(:key, :concurrency, 1, &b) }.to yield_control
|
121
121
|
end
|
122
122
|
end
|
123
|
+
|
124
|
+
context 'with DSL' do
|
125
|
+
it 'instatiates an Unlimiter' do
|
126
|
+
limiter = Berater.new(:key) { unlimited }
|
127
|
+
expect(limiter).to be_a Berater::Unlimiter
|
128
|
+
expect(limiter.key).to be :key
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'instatiates an Inhibiter' do
|
132
|
+
limiter = Berater.new(:key) { inhibited }
|
133
|
+
expect(limiter).to be_a Berater::Inhibitor
|
134
|
+
expect(limiter.key).to be :key
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'instatiates a RateLimiter' do
|
138
|
+
limiter = Berater.new(:key) { 1.per second }
|
139
|
+
expect(limiter).to be_a Berater::RateLimiter
|
140
|
+
expect(limiter.key).to be :key
|
141
|
+
expect(limiter.count).to be 1
|
142
|
+
expect(limiter.interval).to be :second
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'instatiates a ConcurrencyLimiter' do
|
146
|
+
limiter = Berater.new(:key, timeout: 2) { 1.at_once }
|
147
|
+
expect(limiter).to be_a Berater::ConcurrencyLimiter
|
148
|
+
expect(limiter.key).to be :key
|
149
|
+
expect(limiter.capacity).to be 1
|
150
|
+
expect(limiter.timeout).to be 2
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'does not accept mode/args and dsl block' do
|
154
|
+
expect {
|
155
|
+
Berater.new(:key, :rate) { 1.per second }
|
156
|
+
}.to raise_error(ArgumentError)
|
157
|
+
|
158
|
+
expect {
|
159
|
+
Berater.new(:key, :concurrency, 2) { 3.at_once }
|
160
|
+
}.to raise_error(ArgumentError)
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'requires either mode or dsl block' do
|
164
|
+
expect {
|
165
|
+
Berater.new(:key)
|
166
|
+
}.to raise_error(ArgumentError)
|
167
|
+
end
|
168
|
+
end
|
123
169
|
end
|
124
170
|
|
125
171
|
end
|
@@ -1,4 +1,7 @@
|
|
1
1
|
describe Berater::ConcurrencyLimiter do
|
2
|
+
it_behaves_like 'a limiter', described_class.new(:key, 1)
|
3
|
+
it_behaves_like 'a limiter', described_class.new(:key, 1, timeout: 1)
|
4
|
+
|
2
5
|
describe '.new' do
|
3
6
|
let(:limiter) { described_class.new(:key, 1) }
|
4
7
|
|
@@ -61,7 +64,7 @@ describe Berater::ConcurrencyLimiter do
|
|
61
64
|
end
|
62
65
|
|
63
66
|
describe '#limit' do
|
64
|
-
let(:limiter) { described_class.new(:key, 2
|
67
|
+
let(:limiter) { described_class.new(:key, 2) }
|
65
68
|
|
66
69
|
it 'works' do
|
67
70
|
expect {|b| limiter.limit(&b) }.to yield_control
|
@@ -85,18 +88,6 @@ describe Berater::ConcurrencyLimiter do
|
|
85
88
|
expect(limiter).to be_incapacitated
|
86
89
|
end
|
87
90
|
|
88
|
-
it 'times out locks' do
|
89
|
-
expect(limiter.limit).to be_a Berater::Lock
|
90
|
-
expect(limiter.limit).to be_a Berater::Lock
|
91
|
-
expect(limiter).to be_incapacitated
|
92
|
-
|
93
|
-
Timecop.travel(1)
|
94
|
-
|
95
|
-
expect(limiter.limit).to be_a Berater::Lock
|
96
|
-
expect(limiter.limit).to be_a Berater::Lock
|
97
|
-
expect(limiter).to be_incapacitated
|
98
|
-
end
|
99
|
-
|
100
91
|
context 'with capacity 0' do
|
101
92
|
let(:limiter) { described_class.new(:key, 0) }
|
102
93
|
|
@@ -171,4 +162,17 @@ describe Berater::ConcurrencyLimiter do
|
|
171
162
|
end
|
172
163
|
end
|
173
164
|
|
165
|
+
describe '#to_s' do
|
166
|
+
def check(capacity, expected)
|
167
|
+
expect(
|
168
|
+
described_class.new(:key, capacity).to_s
|
169
|
+
).to match(expected)
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'works' do
|
173
|
+
check(1, /1 at a time/)
|
174
|
+
check(3, /3 at a time/)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
174
178
|
end
|
File without changes
|
data/spec/rate_limiter_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
describe Berater::RateLimiter do
|
2
|
+
it_behaves_like 'a limiter', Berater.new(:key, :rate, 3, :second)
|
2
3
|
|
3
4
|
describe '.new' do
|
4
5
|
let(:limiter) { described_class.new(:key, 1, :second) }
|
@@ -6,7 +7,7 @@ describe Berater::RateLimiter do
|
|
6
7
|
it 'initializes' do
|
7
8
|
expect(limiter.key).to be :key
|
8
9
|
expect(limiter.count).to eq 1
|
9
|
-
expect(limiter.interval).to eq
|
10
|
+
expect(limiter.interval).to eq :second
|
10
11
|
end
|
11
12
|
|
12
13
|
it 'has default values' do
|
@@ -51,22 +52,22 @@ describe Berater::RateLimiter do
|
|
51
52
|
end
|
52
53
|
|
53
54
|
context 'with symbols' do
|
54
|
-
it { expect_interval(:sec,
|
55
|
-
it { expect_interval(:second,
|
56
|
-
it { expect_interval(:seconds,
|
55
|
+
it { expect_interval(:sec, :second) }
|
56
|
+
it { expect_interval(:second, :second) }
|
57
|
+
it { expect_interval(:seconds, :second) }
|
57
58
|
|
58
|
-
it { expect_interval(:min,
|
59
|
-
it { expect_interval(:minute,
|
60
|
-
it { expect_interval(:minutes,
|
59
|
+
it { expect_interval(:min, :minute) }
|
60
|
+
it { expect_interval(:minute, :minute) }
|
61
|
+
it { expect_interval(:minutes, :minute) }
|
61
62
|
|
62
|
-
it { expect_interval(:hour,
|
63
|
-
it { expect_interval(:hours,
|
63
|
+
it { expect_interval(:hour, :hour) }
|
64
|
+
it { expect_interval(:hours, :hour) }
|
64
65
|
end
|
65
66
|
|
66
67
|
context 'with strings' do
|
67
|
-
it { expect_interval('sec',
|
68
|
-
it { expect_interval('minute',
|
69
|
-
it { expect_interval('hours',
|
68
|
+
it { expect_interval('sec', :second) }
|
69
|
+
it { expect_interval('minute', :minute) }
|
70
|
+
it { expect_interval('hours', :hour) }
|
70
71
|
end
|
71
72
|
|
72
73
|
context 'with erroneous values' do
|
@@ -80,6 +81,17 @@ describe Berater::RateLimiter do
|
|
80
81
|
it { expect_bad_interval(:secondz) }
|
81
82
|
it { expect_bad_interval('huor') }
|
82
83
|
end
|
84
|
+
|
85
|
+
context 'interprets values' do
|
86
|
+
def expect_sec(interval, expected)
|
87
|
+
limiter = described_class.new(:key, 1, interval)
|
88
|
+
expect(limiter.instance_variable_get(:@interval_sec)).to eq expected
|
89
|
+
end
|
90
|
+
|
91
|
+
it { expect_sec(:second, 1) }
|
92
|
+
it { expect_sec(:minute, 60) }
|
93
|
+
it { expect_sec(:hour, 3600) }
|
94
|
+
end
|
83
95
|
end
|
84
96
|
|
85
97
|
describe '#limit' do
|
@@ -140,4 +152,40 @@ describe Berater::RateLimiter do
|
|
140
152
|
end
|
141
153
|
end
|
142
154
|
|
155
|
+
describe '#to_s' do
|
156
|
+
def check(count, interval, expected)
|
157
|
+
expect(
|
158
|
+
described_class.new(:key, count, interval).to_s
|
159
|
+
).to match(expected)
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'works with symbols' do
|
163
|
+
check(1, :second, /1 per second/)
|
164
|
+
check(1, :minute, /1 per minute/)
|
165
|
+
check(1, :hour, /1 per hour/)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'works with strings' do
|
169
|
+
check(1, 'second', /1 per second/)
|
170
|
+
check(1, 'minute', /1 per minute/)
|
171
|
+
check(1, 'hour', /1 per hour/)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'normalizes' do
|
175
|
+
check(1, :sec, /1 per second/)
|
176
|
+
check(1, :seconds, /1 per second/)
|
177
|
+
|
178
|
+
check(1, :min, /1 per minute/)
|
179
|
+
check(1, :minutes, /1 per minute/)
|
180
|
+
|
181
|
+
check(1, :hours, /1 per hour/)
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'works with integers' do
|
185
|
+
check(1, 1, /1 every second/)
|
186
|
+
check(1, 2, /1 every 2 seconds/)
|
187
|
+
check(2, 3, /2 every 3 seconds/)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
143
191
|
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'berater/test_mode'
|
2
|
+
|
3
|
+
describe 'Berater.test_mode' do
|
4
|
+
after { Berater.test_mode = nil }
|
5
|
+
|
6
|
+
describe 'Unlimiter' do
|
7
|
+
let(:limiter) { Berater::Unlimiter.new }
|
8
|
+
|
9
|
+
context 'when test_mode = nil' do
|
10
|
+
before { Berater.test_mode = nil }
|
11
|
+
|
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
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when test_mode = :pass' do
|
21
|
+
before { Berater.test_mode = :pass }
|
22
|
+
|
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
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when test_mode = :fail' do
|
32
|
+
before { Berater.test_mode = :fail }
|
33
|
+
|
34
|
+
it { expect(limiter).to be_a Berater::Unlimiter }
|
35
|
+
|
36
|
+
it 'never works' do
|
37
|
+
expect { limiter }.to be_overloaded
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'Inhibitor' do
|
43
|
+
let(:limiter) { Berater::Inhibitor.new }
|
44
|
+
|
45
|
+
context 'when test_mode = nil' do
|
46
|
+
before { Berater.test_mode = nil }
|
47
|
+
|
48
|
+
it { expect(limiter).to be_a Berater::Inhibitor }
|
49
|
+
|
50
|
+
it 'works per usual' do
|
51
|
+
expect { limiter }.to be_overloaded
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when test_mode = :pass' do
|
56
|
+
before { Berater.test_mode = :pass }
|
57
|
+
|
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
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'when test_mode = :fail' do
|
67
|
+
before { Berater.test_mode = :fail }
|
68
|
+
|
69
|
+
it { expect(limiter).to be_a Berater::Inhibitor }
|
70
|
+
|
71
|
+
it 'never works' do
|
72
|
+
expect { limiter }.to be_overloaded
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe 'RateLimiter' do
|
78
|
+
let(:limiter) { Berater::RateLimiter.new(:key, 1, :second) }
|
79
|
+
|
80
|
+
shared_examples 'a RateLimiter' do
|
81
|
+
it { expect(limiter).to be_a Berater::RateLimiter }
|
82
|
+
|
83
|
+
it 'checks arguments' do
|
84
|
+
expect {
|
85
|
+
Berater::RateLimiter.new(:key, 1)
|
86
|
+
}.to raise_error(ArgumentError)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'when test_mode = nil' do
|
91
|
+
before { Berater.test_mode = nil }
|
92
|
+
|
93
|
+
it_behaves_like 'a RateLimiter'
|
94
|
+
|
95
|
+
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
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'when test_mode = :pass' do
|
107
|
+
before { Berater.test_mode = :pass }
|
108
|
+
|
109
|
+
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
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'when test_mode = :fail' do
|
119
|
+
before { Berater.test_mode = :fail }
|
120
|
+
|
121
|
+
it_behaves_like 'a RateLimiter'
|
122
|
+
|
123
|
+
it 'never works and without calling redis' do
|
124
|
+
expect(limiter.redis).not_to receive(:multi)
|
125
|
+
expect { limiter }.to be_overloaded
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
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
|
142
|
+
|
143
|
+
context 'when test_mode = nil' do
|
144
|
+
before { Berater.test_mode = nil }
|
145
|
+
|
146
|
+
it_behaves_like 'a ConcurrencyLimiter'
|
147
|
+
|
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
|
153
|
+
|
154
|
+
it 'yields per usual' do
|
155
|
+
expect {|block| limiter.limit(&block) }.to yield_control
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
context 'when test_mode = :pass' do
|
160
|
+
before { Berater.test_mode = :pass }
|
161
|
+
|
162
|
+
it_behaves_like 'a ConcurrencyLimiter'
|
163
|
+
|
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
|
169
|
+
end
|
170
|
+
|
171
|
+
context 'when test_mode = :fail' do
|
172
|
+
before { Berater.test_mode = :fail }
|
173
|
+
|
174
|
+
it_behaves_like 'a ConcurrencyLimiter'
|
175
|
+
|
176
|
+
it 'never works and without calling redis' do
|
177
|
+
expect(limiter.redis).not_to receive(:eval)
|
178
|
+
expect { limiter }.to be_overloaded
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
data/spec/unlimiter_spec.rb
CHANGED
@@ -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
|
@@ -31,7 +33,4 @@ describe Berater::Unlimiter do
|
|
31
33
|
end
|
32
34
|
end
|
33
35
|
end
|
34
|
-
|
35
|
-
it_behaves_like 'a lock', described_class.new
|
36
|
-
|
37
36
|
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.
|
4
|
+
version: 0.3.0
|
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-
|
11
|
+
date: 2021-02-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -115,20 +115,23 @@ extensions: []
|
|
115
115
|
extra_rdoc_files: []
|
116
116
|
files:
|
117
117
|
- lib/berater.rb
|
118
|
-
- lib/berater/base_limiter.rb
|
119
118
|
- lib/berater/concurrency_limiter.rb
|
119
|
+
- lib/berater/dsl.rb
|
120
120
|
- lib/berater/inhibitor.rb
|
121
|
+
- lib/berater/limiter.rb
|
121
122
|
- lib/berater/lock.rb
|
122
123
|
- lib/berater/rate_limiter.rb
|
124
|
+
- lib/berater/rspec.rb
|
125
|
+
- lib/berater/rspec/matchers.rb
|
126
|
+
- lib/berater/test_mode.rb
|
123
127
|
- lib/berater/unlimiter.rb
|
124
128
|
- lib/berater/version.rb
|
125
129
|
- spec/berater_spec.rb
|
126
130
|
- spec/concurrency_limiter_spec.rb
|
127
|
-
- spec/concurrency_lock_spec.rb
|
128
131
|
- spec/inhibitor_spec.rb
|
129
|
-
- spec/
|
132
|
+
- spec/matchers_spec.rb
|
130
133
|
- spec/rate_limiter_spec.rb
|
131
|
-
- spec/
|
134
|
+
- spec/test_mode_spec.rb
|
132
135
|
- spec/unlimiter_spec.rb
|
133
136
|
homepage: https://github.com/dpep/berater_rb
|
134
137
|
licenses:
|
@@ -155,10 +158,9 @@ specification_version: 4
|
|
155
158
|
summary: Berater
|
156
159
|
test_files:
|
157
160
|
- spec/rate_limiter_spec.rb
|
158
|
-
- spec/
|
159
|
-
- spec/
|
161
|
+
- spec/matchers_spec.rb
|
162
|
+
- spec/test_mode_spec.rb
|
160
163
|
- spec/concurrency_limiter_spec.rb
|
161
|
-
- spec/concurrency_lock_spec.rb
|
162
164
|
- spec/berater_spec.rb
|
163
165
|
- spec/inhibitor_spec.rb
|
164
166
|
- spec/unlimiter_spec.rb
|
@@ -1,39 +0,0 @@
|
|
1
|
-
describe Berater::Lock do
|
2
|
-
it_behaves_like 'a lock', Berater.new(:key, :concurrency, 3)
|
3
|
-
|
4
|
-
let(:limiter) { Berater.new(:key, :concurrency, 3) }
|
5
|
-
|
6
|
-
describe '#expired?' do
|
7
|
-
let!(:lock) { limiter.limit }
|
8
|
-
|
9
|
-
context 'when timeout is not set' do
|
10
|
-
it { expect(limiter.timeout).to eq 0 }
|
11
|
-
|
12
|
-
it 'never expires' do
|
13
|
-
expect(lock.locked?).to be true
|
14
|
-
expect(lock.expired?).to be false
|
15
|
-
|
16
|
-
Timecop.travel(1_000)
|
17
|
-
|
18
|
-
expect(lock.locked?).to be true
|
19
|
-
expect(lock.expired?).to be false
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
context 'when timeout is set and exceeded' do
|
24
|
-
before { Timecop.travel(1) }
|
25
|
-
|
26
|
-
let(:limiter) { Berater.new(:key, :concurrency, 3, timeout: 1) }
|
27
|
-
|
28
|
-
it 'expires' do
|
29
|
-
expect(lock.expired?).to be true
|
30
|
-
expect(lock.locked?).to be false
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'fails to release' do
|
34
|
-
expect { lock.release }.to raise_error(RuntimeError, /expired/)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
end
|
data/spec/rate_lock_spec.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
describe Berater::Lock do
|
2
|
-
it_behaves_like 'a lock', Berater.new(:key, :rate, 3, :second)
|
3
|
-
|
4
|
-
let(:limiter) { Berater.new(:key, :rate, 3, :second) }
|
5
|
-
|
6
|
-
describe '#expired?' do
|
7
|
-
let!(:lock) { limiter.limit }
|
8
|
-
|
9
|
-
it 'never expires' do
|
10
|
-
expect(lock.locked?).to be true
|
11
|
-
expect(lock.expired?).to be false
|
12
|
-
|
13
|
-
Timecop.travel(1_000)
|
14
|
-
|
15
|
-
expect(lock.locked?).to be true
|
16
|
-
expect(lock.expired?).to be false
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|