berater 0.6.0 → 0.8.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 +14 -13
- data/lib/berater/concurrency_limiter.rb +19 -37
- data/lib/berater/dsl.rb +8 -8
- data/lib/berater/inhibitor.rb +4 -7
- data/lib/berater/limiter.rb +49 -19
- data/lib/berater/lock.rb +2 -1
- data/lib/berater/rate_limiter.rb +40 -40
- data/lib/berater/rspec.rb +1 -2
- data/lib/berater/rspec/matchers.rb +26 -48
- data/lib/berater/static_limiter.rb +49 -0
- data/lib/berater/test_mode.rb +31 -36
- data/lib/berater/unlimiter.rb +8 -6
- data/lib/berater/utils.rb +9 -0
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +26 -76
- data/spec/concurrency_limiter_spec.rb +73 -57
- data/spec/dsl_refinement_spec.rb +0 -12
- data/spec/dsl_spec.rb +5 -17
- data/spec/inhibitor_spec.rb +10 -5
- data/spec/limiter_spec.rb +95 -9
- data/spec/matchers_spec.rb +21 -85
- data/spec/rate_limiter_spec.rb +88 -38
- data/spec/riddle_spec.rb +6 -2
- data/spec/static_limiter_spec.rb +79 -0
- data/spec/test_mode_spec.rb +48 -106
- data/spec/unlimiter_spec.rb +11 -5
- metadata +5 -2
data/lib/berater/rspec.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
require 'berater'
|
2
2
|
require 'berater/rspec/matchers'
|
3
3
|
require 'berater/test_mode'
|
4
|
-
require 'rspec'
|
4
|
+
require 'rspec/core'
|
5
5
|
|
6
6
|
RSpec.configure do |config|
|
7
7
|
config.include(Berater::Matchers)
|
8
8
|
|
9
9
|
config.after do
|
10
10
|
Berater.expunge rescue nil
|
11
|
-
Berater.redis.script(:flush) rescue nil
|
12
11
|
Berater.reset
|
13
12
|
end
|
14
13
|
end
|
@@ -1,83 +1,61 @@
|
|
1
1
|
module Berater
|
2
2
|
module Matchers
|
3
3
|
class Overloaded
|
4
|
-
def initialize(type)
|
5
|
-
@type = type
|
6
|
-
end
|
7
|
-
|
8
4
|
def supports_block_expectations?
|
9
5
|
true
|
10
6
|
end
|
11
7
|
|
12
8
|
def matches?(obj)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
false
|
27
|
-
end
|
28
|
-
when Berater::Limiter
|
29
|
-
# eg. expect(Berater.new(...)).to be_overloaded
|
30
|
-
@limiter = obj
|
31
|
-
obj.overloaded?
|
9
|
+
case obj
|
10
|
+
when Proc
|
11
|
+
# eg. expect { ... }.to be_overloaded
|
12
|
+
res = obj.call
|
13
|
+
|
14
|
+
if res.is_a? Berater::Limiter
|
15
|
+
# eg. expect { Berater.new(...) }.to be_overloaded
|
16
|
+
@limiter = res
|
17
|
+
@limiter.utilization >= 1
|
18
|
+
else
|
19
|
+
# eg. expect { Berater(...) }.to be_overloaded
|
20
|
+
# eg. expect { limiter.limit }.to be_overloaded
|
21
|
+
false
|
32
22
|
end
|
33
|
-
|
34
|
-
|
23
|
+
when Berater::Limiter
|
24
|
+
# eg. expect(Berater.new(...)).to be_overloaded
|
25
|
+
@limiter = obj
|
26
|
+
@limiter.utilization >= 1
|
35
27
|
end
|
28
|
+
rescue Berater::Overloaded
|
29
|
+
true
|
36
30
|
end
|
37
31
|
|
38
32
|
def description
|
39
33
|
if @limiter
|
40
|
-
"be
|
34
|
+
"be overloaded"
|
41
35
|
else
|
42
|
-
"raise #{
|
36
|
+
"raise #{Berater::Overloaded}"
|
43
37
|
end
|
44
38
|
end
|
45
39
|
|
46
40
|
def failure_message
|
47
41
|
if @limiter
|
48
|
-
"expected to be
|
42
|
+
"expected to be overloaded"
|
49
43
|
else
|
50
|
-
"expected #{
|
44
|
+
"expected #{Berater::Overloaded} to be raised"
|
51
45
|
end
|
52
46
|
end
|
53
47
|
|
54
48
|
def failure_message_when_negated
|
55
49
|
if @limiter
|
56
|
-
"expected not to be
|
50
|
+
"expected not to be overloaded"
|
57
51
|
else
|
58
|
-
"did not expect #{
|
52
|
+
"did not expect #{Berater::Overloaded} to be raised"
|
59
53
|
end
|
60
54
|
end
|
61
|
-
|
62
|
-
private def verb
|
63
|
-
@type.to_s.split('::')[-1].downcase
|
64
|
-
end
|
65
55
|
end
|
66
56
|
|
67
57
|
def be_overloaded
|
68
|
-
Overloaded.new
|
69
|
-
end
|
70
|
-
|
71
|
-
def be_overrated
|
72
|
-
Overloaded.new(Berater::RateLimiter::Overrated)
|
73
|
-
end
|
74
|
-
|
75
|
-
def be_incapacitated
|
76
|
-
Overloaded.new(Berater::ConcurrencyLimiter::Incapacitated)
|
77
|
-
end
|
78
|
-
|
79
|
-
def be_inhibited
|
80
|
-
Overloaded.new(Berater::Inhibitor::Inhibited)
|
58
|
+
Overloaded.new
|
81
59
|
end
|
82
60
|
end
|
83
61
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Berater
|
2
|
+
class StaticLimiter < Limiter
|
3
|
+
|
4
|
+
LUA_SCRIPT = Berater::LuaScript(<<~LUA
|
5
|
+
local key = KEYS[1]
|
6
|
+
local capacity = tonumber(ARGV[1])
|
7
|
+
local cost = tonumber(ARGV[2])
|
8
|
+
|
9
|
+
local count = redis.call('GET', key) or 0
|
10
|
+
local allowed = (count + cost) <= capacity
|
11
|
+
|
12
|
+
if allowed then
|
13
|
+
count = count + cost
|
14
|
+
redis.call('SET', key, count)
|
15
|
+
end
|
16
|
+
|
17
|
+
return { tostring(count), allowed }
|
18
|
+
LUA
|
19
|
+
)
|
20
|
+
|
21
|
+
protected def acquire_lock(capacity, cost)
|
22
|
+
if cost == 0
|
23
|
+
# utilization check
|
24
|
+
count = redis.get(cache_key) || "0"
|
25
|
+
allowed = true
|
26
|
+
else
|
27
|
+
count, allowed = LUA_SCRIPT.eval(
|
28
|
+
redis, [ cache_key ], [ capacity, cost ],
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Redis returns Floats as strings to maintain precision
|
33
|
+
count = count.include?('.') ? count.to_f : count.to_i
|
34
|
+
|
35
|
+
raise Overloaded unless allowed
|
36
|
+
|
37
|
+
release_fn = if cost > 0
|
38
|
+
proc { redis.incrbyfloat(cache_key, -cost) }
|
39
|
+
end
|
40
|
+
|
41
|
+
Lock.new(capacity, count, release_fn)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
"#<#{self.class}(#{key}: #{capacity})>"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
data/lib/berater/test_mode.rb
CHANGED
@@ -1,52 +1,47 @@
|
|
1
1
|
require 'berater'
|
2
2
|
|
3
3
|
module Berater
|
4
|
-
extend self
|
5
4
|
|
6
|
-
|
5
|
+
module TestMode
|
6
|
+
attr_reader :test_mode
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
def test_mode=(mode)
|
9
|
+
unless [ nil, :pass, :fail ].include?(mode)
|
10
|
+
raise ArgumentError, "invalid mode: #{Berater.test_mode}"
|
11
|
+
end
|
12
12
|
|
13
|
-
|
13
|
+
@test_mode = mode
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
def reset
|
17
|
+
super
|
18
|
+
@test_mode = nil
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
# don't stub self
|
34
|
-
return super if self < stub_klass
|
35
|
-
|
36
|
-
# swap out limit and overloaded? methods with stub
|
37
|
-
super.tap do |instance|
|
38
|
-
stub = stub_klass.allocate
|
39
|
-
stub.send(:initialize, *args, **opts)
|
40
|
-
|
41
|
-
instance.define_singleton_method(:limit) do |**opts, &block|
|
42
|
-
stub.limit(**opts, &block)
|
43
|
-
end
|
44
|
-
|
45
|
-
instance.define_singleton_method(:overloaded?) do
|
46
|
-
stub.overloaded?
|
22
|
+
class Limiter
|
23
|
+
module TestMode
|
24
|
+
def acquire_lock(*)
|
25
|
+
case Berater.test_mode
|
26
|
+
when :pass
|
27
|
+
Lock.new(Float::INFINITY, 0)
|
28
|
+
when :fail
|
29
|
+
raise Overloaded
|
30
|
+
else
|
31
|
+
super
|
47
32
|
end
|
48
33
|
end
|
49
34
|
end
|
50
35
|
end
|
51
36
|
|
52
37
|
end
|
38
|
+
|
39
|
+
# prepend class methods
|
40
|
+
Berater.singleton_class.prepend Berater::TestMode
|
41
|
+
|
42
|
+
# stub each Limiter subclass
|
43
|
+
ObjectSpace.each_object(Class).each do |klass|
|
44
|
+
next unless klass < Berater::Limiter
|
45
|
+
|
46
|
+
klass.prepend Berater::Limiter::TestMode
|
47
|
+
end
|
data/lib/berater/unlimiter.rb
CHANGED
@@ -5,17 +5,19 @@ module Berater
|
|
5
5
|
super(key, Float::INFINITY, **opts)
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
9
|
-
|
8
|
+
def to_s
|
9
|
+
"#<#{self.class}>"
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
false
|
14
|
-
end
|
12
|
+
protected
|
15
13
|
|
16
|
-
|
14
|
+
def capacity=(*)
|
17
15
|
@capacity = Float::INFINITY
|
18
16
|
end
|
19
17
|
|
18
|
+
def acquire_lock(*)
|
19
|
+
Lock.new(Float::INFINITY, 0)
|
20
|
+
end
|
21
|
+
|
20
22
|
end
|
21
23
|
end
|
data/lib/berater/utils.rb
CHANGED
data/lib/berater/version.rb
CHANGED
data/spec/berater_spec.rb
CHANGED
@@ -24,50 +24,15 @@ describe Berater do
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
let(:limiter) { Berater.new(:key,
|
27
|
+
shared_examples 'a Berater' do |klass, capacity, **opts|
|
28
|
+
describe '.new' do
|
29
|
+
let(:limiter) { Berater.new(:key, capacity, **opts) }
|
30
30
|
|
31
|
-
it 'instantiates
|
32
|
-
expect(limiter).to be_a
|
33
|
-
expect(limiter.key).to be :key
|
34
|
-
end
|
35
|
-
|
36
|
-
it 'inherits redis' do
|
37
|
-
expect(limiter.redis).to be Berater.redis
|
38
|
-
end
|
39
|
-
|
40
|
-
it 'accepts options' do
|
41
|
-
redis = double('Redis')
|
42
|
-
limiter = Berater.new(:key, Float::INFINITY, redis: redis)
|
43
|
-
expect(limiter.redis).to be redis
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
context 'inhibited mode' do
|
48
|
-
let(:limiter) { Berater.new(:key, 0) }
|
49
|
-
|
50
|
-
it 'instantiates an Inhibitor' do
|
51
|
-
expect(limiter).to be_a Berater::Inhibitor
|
52
|
-
expect(limiter.key).to be :key
|
53
|
-
end
|
54
|
-
|
55
|
-
it 'inherits redis' do
|
56
|
-
expect(limiter.redis).to be Berater.redis
|
57
|
-
end
|
58
|
-
|
59
|
-
it 'accepts options' do
|
60
|
-
redis = double('Redis')
|
61
|
-
limiter = Berater.new(:key, 0, redis: redis)
|
62
|
-
expect(limiter.redis).to be redis
|
31
|
+
it 'instantiates the right class' do
|
32
|
+
expect(limiter).to be_a klass
|
63
33
|
end
|
64
|
-
end
|
65
34
|
|
66
|
-
|
67
|
-
let(:limiter) { Berater.new(:key, 1, :second) }
|
68
|
-
|
69
|
-
it 'instantiates a RateLimiter' do
|
70
|
-
expect(limiter).to be_a Berater::RateLimiter
|
35
|
+
it 'sets the key' do
|
71
36
|
expect(limiter.key).to be :key
|
72
37
|
end
|
73
38
|
|
@@ -75,60 +40,45 @@ describe Berater do
|
|
75
40
|
expect(limiter.redis).to be Berater.redis
|
76
41
|
end
|
77
42
|
|
78
|
-
it 'accepts
|
43
|
+
it 'accepts an optional redis parameter' do
|
79
44
|
redis = double('Redis')
|
80
|
-
limiter = Berater.new(:key,
|
45
|
+
limiter = Berater.new(:key, capacity, opts.merge(redis: redis))
|
81
46
|
expect(limiter.redis).to be redis
|
82
47
|
end
|
83
48
|
end
|
84
49
|
|
85
|
-
|
86
|
-
let(:limiter) { Berater
|
50
|
+
describe 'Berater() convenience method' do
|
51
|
+
let(:limiter) { Berater(:key, capacity, **opts) }
|
87
52
|
|
88
|
-
it '
|
89
|
-
expect(limiter).to be_a
|
90
|
-
expect(limiter.key).to be :key
|
53
|
+
it 'creates a limiter' do
|
54
|
+
expect(limiter).to be_a klass
|
91
55
|
end
|
92
56
|
|
93
|
-
it '
|
94
|
-
expect(limiter
|
57
|
+
it 'creates an equivalent limiter' do
|
58
|
+
expect(limiter).to eq Berater.new(:key, capacity, **opts)
|
95
59
|
end
|
96
60
|
|
97
|
-
|
98
|
-
|
99
|
-
limiter = Berater.new(:key, 1, redis: redis)
|
100
|
-
expect(limiter.redis).to be redis
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
61
|
+
context 'with a block' do
|
62
|
+
before { Berater.test_mode = :pass }
|
104
63
|
|
105
|
-
|
106
|
-
RSpec.shared_examples 'test convenience' do |klass, *args|
|
107
|
-
it 'creates a limiter' do
|
108
|
-
limiter = Berater(:key, *args)
|
109
|
-
expect(limiter).to be_a klass
|
110
|
-
end
|
64
|
+
subject { Berater(:key, capacity, **opts) { 123 } }
|
111
65
|
|
112
|
-
context 'with a block' do
|
113
66
|
it 'creates a limiter and calls limit' do
|
114
|
-
limiter = Berater(:key, *args)
|
115
67
|
expect(klass).to receive(:new).and_return(limiter)
|
116
68
|
expect(limiter).to receive(:limit).and_call_original
|
69
|
+
subject
|
70
|
+
end
|
117
71
|
|
118
|
-
|
119
|
-
|
120
|
-
expect(res).to be true
|
121
|
-
rescue Berater::Overloaded
|
122
|
-
expect(klass).to be Berater::Inhibitor
|
123
|
-
end
|
72
|
+
it 'yields' do
|
73
|
+
is_expected.to be 123
|
124
74
|
end
|
125
75
|
end
|
126
76
|
end
|
127
|
-
|
128
|
-
include_examples 'test convenience', Berater::Unlimiter, Float::INFINITY
|
129
|
-
include_examples 'test convenience', Berater::Inhibitor, 0
|
130
|
-
include_examples 'test convenience', Berater::RateLimiter, 1, :second
|
131
|
-
include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
|
132
77
|
end
|
133
78
|
|
79
|
+
include_examples 'a Berater', Berater::ConcurrencyLimiter, 1, timeout: 1
|
80
|
+
include_examples 'a Berater', Berater::Inhibitor, 0
|
81
|
+
include_examples 'a Berater', Berater::RateLimiter, 1, interval: :second
|
82
|
+
include_examples 'a Berater', Berater::StaticLimiter, 1
|
83
|
+
include_examples 'a Berater', Berater::Unlimiter, Float::INFINITY
|
134
84
|
end
|