berater 0.6.2 → 0.7.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 +10 -7
- data/lib/berater/concurrency_limiter.rb +8 -11
- data/lib/berater/dsl.rb +8 -8
- data/lib/berater/inhibitor.rb +1 -5
- data/lib/berater/limiter.rb +12 -6
- data/lib/berater/rate_limiter.rb +31 -24
- data/lib/berater/rspec/matchers.rb +11 -31
- data/lib/berater/test_mode.rb +1 -8
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +9 -9
- data/spec/concurrency_limiter_spec.rb +51 -51
- data/spec/dsl_refinement_spec.rb +0 -12
- data/spec/dsl_spec.rb +5 -17
- data/spec/limiter_spec.rb +1 -1
- data/spec/matchers_spec.rb +21 -85
- data/spec/rate_limiter_spec.rb +86 -37
- data/spec/riddle_spec.rb +6 -2
- data/spec/test_mode_spec.rb +19 -102
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d25d186bfb9e709986e5c205c9fd2b8107dcf8df1e915316d28a6a59a0d4c0f
|
4
|
+
data.tar.gz: '05380448ad96697b5d3753a93584d4bdfcf3b028c649ca11360a1c92d6afadad'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ddcd488d3d1aa293d621f32ad8cd684daffb0ba9c413e8cf6a7a292ae63a307e2cdf8e62803cf2a8c2b3ea7b9424e4874bb773f49620e768895b193385e847f
|
7
|
+
data.tar.gz: cf923f95875da8b7e6c0c0cbe4dccad5a4e3c9f6436da606a03f93c2b9ff86525485c6379ade0f276f30e1aff3bcf0c279fe88d33e6769455818482a5ff1a179
|
data/lib/berater.rb
CHANGED
@@ -19,20 +19,23 @@ module Berater
|
|
19
19
|
@redis = nil
|
20
20
|
end
|
21
21
|
|
22
|
-
def new(key, capacity,
|
22
|
+
def new(key, capacity, **opts)
|
23
|
+
args = []
|
24
|
+
|
23
25
|
case capacity
|
24
|
-
when
|
26
|
+
when Float::INFINITY
|
25
27
|
Berater::Unlimiter
|
26
|
-
when
|
28
|
+
when 0
|
27
29
|
Berater::Inhibitor
|
28
30
|
else
|
29
|
-
if interval
|
31
|
+
if opts[:interval]
|
32
|
+
args << opts.delete(:interval)
|
30
33
|
Berater::RateLimiter
|
31
34
|
else
|
32
35
|
Berater::ConcurrencyLimiter
|
33
36
|
end
|
34
37
|
end.yield_self do |klass|
|
35
|
-
args = [ key, capacity,
|
38
|
+
args = [ key, capacity, *args ].compact
|
36
39
|
klass.new(*args, **opts)
|
37
40
|
end
|
38
41
|
end
|
@@ -46,8 +49,8 @@ module Berater
|
|
46
49
|
end
|
47
50
|
|
48
51
|
# convenience method
|
49
|
-
def Berater(key, capacity,
|
50
|
-
limiter = Berater.new(key, capacity,
|
52
|
+
def Berater(key, capacity, **opts, &block)
|
53
|
+
limiter = Berater.new(key, capacity, **opts)
|
51
54
|
if block_given?
|
52
55
|
limiter.limit(&block)
|
53
56
|
else
|
@@ -1,13 +1,14 @@
|
|
1
1
|
module Berater
|
2
2
|
class ConcurrencyLimiter < Limiter
|
3
3
|
|
4
|
-
class Incapacitated < Overloaded; end
|
5
|
-
|
6
4
|
attr_reader :timeout
|
7
5
|
|
8
6
|
def initialize(key, capacity, **opts)
|
9
7
|
super(key, capacity, **opts)
|
10
8
|
|
9
|
+
# round fractional capacity
|
10
|
+
self.capacity = capacity.to_i
|
11
|
+
|
11
12
|
self.timeout = opts[:timeout] || 0
|
12
13
|
end
|
13
14
|
|
@@ -28,17 +29,15 @@ module Berater
|
|
28
29
|
|
29
30
|
-- purge stale hosts
|
30
31
|
if ttl > 0 then
|
31
|
-
redis.call('ZREMRANGEBYSCORE', key,
|
32
|
+
redis.call('ZREMRANGEBYSCORE', key, 0, ts - ttl)
|
32
33
|
end
|
33
34
|
|
34
35
|
-- check capacity
|
35
36
|
local count = redis.call('ZCARD', key)
|
36
37
|
|
37
38
|
if cost == 0 then
|
38
|
-
-- just
|
39
|
-
|
40
|
-
table.insert(lock_ids, true)
|
41
|
-
end
|
39
|
+
-- just checking count
|
40
|
+
table.insert(lock_ids, true)
|
42
41
|
elseif (count + cost) <= capacity then
|
43
42
|
-- grab locks, one per cost
|
44
43
|
local lock_id = redis.call('INCRBY', lock_key, cost)
|
@@ -64,7 +63,7 @@ module Berater
|
|
64
63
|
)
|
65
64
|
|
66
65
|
protected def acquire_lock(capacity, cost)
|
67
|
-
# fractional
|
66
|
+
# round fractional capacity and cost
|
68
67
|
capacity = capacity.to_i
|
69
68
|
cost = cost.ceil
|
70
69
|
|
@@ -77,7 +76,7 @@ module Berater
|
|
77
76
|
[ capacity, ts, @timeout_msec, cost ]
|
78
77
|
)
|
79
78
|
|
80
|
-
raise
|
79
|
+
raise Overloaded if lock_ids.empty?
|
81
80
|
|
82
81
|
release_fn = if cost > 0
|
83
82
|
proc { release(lock_ids) }
|
@@ -86,8 +85,6 @@ module Berater
|
|
86
85
|
Lock.new(capacity, count, release_fn)
|
87
86
|
end
|
88
87
|
|
89
|
-
alias incapacitated? overloaded?
|
90
|
-
|
91
88
|
private def release(lock_ids)
|
92
89
|
res = redis.zrem(cache_key(key), lock_ids)
|
93
90
|
res == true || res == lock_ids.count # depending on which version of Redis
|
data/lib/berater/dsl.rb
CHANGED
@@ -1,20 +1,21 @@
|
|
1
1
|
module Berater
|
2
2
|
module DSL
|
3
3
|
refine Berater.singleton_class do
|
4
|
-
def new(key,
|
5
|
-
if
|
4
|
+
def new(key, capacity = nil, **opts, &block)
|
5
|
+
if capacity.nil?
|
6
6
|
unless block_given?
|
7
|
-
raise ArgumentError, 'expected either
|
7
|
+
raise ArgumentError, 'expected either capacity or block'
|
8
8
|
end
|
9
9
|
|
10
|
-
|
10
|
+
capacity, more_opts = DSL.eval(&block)
|
11
|
+
opts.merge!(more_opts) if more_opts
|
11
12
|
else
|
12
13
|
if block_given?
|
13
|
-
raise ArgumentError, 'expected either
|
14
|
+
raise ArgumentError, 'expected either capacity or block, not both'
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
|
-
super(key,
|
18
|
+
super(key, capacity, **opts)
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
@@ -38,13 +39,12 @@ module Berater
|
|
38
39
|
|
39
40
|
KEYWORDS = [
|
40
41
|
:second, :minute, :hour,
|
41
|
-
:unlimited, :inhibited,
|
42
42
|
].freeze
|
43
43
|
|
44
44
|
def install
|
45
45
|
Integer.class_eval do
|
46
46
|
def per(unit)
|
47
|
-
[ self, unit ]
|
47
|
+
[ self, interval: unit ]
|
48
48
|
end
|
49
49
|
alias every per
|
50
50
|
|
data/lib/berater/inhibitor.rb
CHANGED
@@ -1,16 +1,12 @@
|
|
1
1
|
module Berater
|
2
2
|
class Inhibitor < Limiter
|
3
3
|
|
4
|
-
class Inhibited < Overloaded; end
|
5
|
-
|
6
4
|
def initialize(key = :inhibitor, *args, **opts)
|
7
5
|
super(key, 0, **opts)
|
8
6
|
end
|
9
7
|
|
10
|
-
alias inhibited? overloaded?
|
11
|
-
|
12
8
|
protected def acquire_lock(*)
|
13
|
-
raise
|
9
|
+
raise Overloaded
|
14
10
|
end
|
15
11
|
|
16
12
|
end
|
data/lib/berater/limiter.rb
CHANGED
@@ -10,11 +10,11 @@ module Berater
|
|
10
10
|
def limit(capacity: nil, cost: 1, &block)
|
11
11
|
capacity ||= @capacity
|
12
12
|
|
13
|
-
unless capacity.is_a?(Numeric)
|
13
|
+
unless capacity.is_a?(Numeric) && capacity >= 0
|
14
14
|
raise ArgumentError, "invalid capacity: #{capacity}"
|
15
15
|
end
|
16
16
|
|
17
|
-
unless cost.is_a?(Numeric) && cost >= 0
|
17
|
+
unless cost.is_a?(Numeric) && cost >= 0 && cost < Float::INFINITY
|
18
18
|
raise ArgumentError, "invalid cost: #{cost}"
|
19
19
|
end
|
20
20
|
|
@@ -31,10 +31,16 @@ module Berater
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
35
|
-
limit(cost: 0)
|
36
|
-
|
37
|
-
|
34
|
+
def utilization
|
35
|
+
lock = limit(cost: 0)
|
36
|
+
|
37
|
+
if lock.capacity == 0
|
38
|
+
1.0
|
39
|
+
else
|
40
|
+
lock.contention.to_f / lock.capacity
|
41
|
+
end
|
42
|
+
rescue Berater::Overloaded
|
43
|
+
1.0
|
38
44
|
end
|
39
45
|
|
40
46
|
def to_s
|
data/lib/berater/rate_limiter.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
module Berater
|
2
2
|
class RateLimiter < Limiter
|
3
3
|
|
4
|
-
class Overrated < Overloaded; end
|
5
|
-
|
6
4
|
attr_accessor :interval
|
7
5
|
|
8
6
|
def initialize(key, capacity, interval, **opts)
|
@@ -26,24 +24,32 @@ module Berater
|
|
26
24
|
local capacity = tonumber(ARGV[2])
|
27
25
|
local interval_msec = tonumber(ARGV[3])
|
28
26
|
local cost = tonumber(ARGV[4])
|
29
|
-
local count = 0
|
30
|
-
local allowed
|
31
|
-
local msec_per_drip = interval_msec / capacity
|
32
|
-
|
33
|
-
-- timestamp of last update
|
34
|
-
local last_ts = tonumber(redis.call('GET', ts_key))
|
35
27
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
28
|
+
local allowed -- whether lock was acquired
|
29
|
+
local count -- capacity being utilized
|
30
|
+
local msec_per_drip = interval_msec / capacity
|
31
|
+
local state = redis.call('GET', key)
|
32
|
+
|
33
|
+
if state then
|
34
|
+
local last_ts -- timestamp of last update
|
35
|
+
count, last_ts = string.match(state, '([%d.]+);(%w+)')
|
36
|
+
count = tonumber(count)
|
37
|
+
last_ts = tonumber(last_ts, 16)
|
38
|
+
|
39
|
+
-- adjust for time passing, guarding against clock skew
|
40
|
+
if ts > last_ts then
|
41
|
+
local drips = math.floor((ts - last_ts) / msec_per_drip)
|
42
|
+
count = math.max(0, count - drips)
|
43
|
+
else
|
44
|
+
ts = last_ts
|
45
|
+
end
|
46
|
+
else
|
47
|
+
count = 0
|
42
48
|
end
|
43
49
|
|
44
50
|
if cost == 0 then
|
45
|
-
-- just
|
46
|
-
allowed =
|
51
|
+
-- just checking count
|
52
|
+
allowed = true
|
47
53
|
else
|
48
54
|
allowed = (count + cost) <= capacity
|
49
55
|
|
@@ -52,14 +58,15 @@ module Berater
|
|
52
58
|
|
53
59
|
-- time for bucket to empty, in milliseconds
|
54
60
|
local ttl = math.ceil(count * msec_per_drip)
|
61
|
+
ttl = ttl + 100 -- margin of error, for clock skew
|
55
62
|
|
56
|
-
-- update count and last_ts, with
|
57
|
-
|
58
|
-
redis.call('SET',
|
63
|
+
-- update count and last_ts, with expiration
|
64
|
+
state = string.format('%f;%X', count, ts)
|
65
|
+
redis.call('SET', key, state, 'PX', ttl)
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
62
|
-
return { count, allowed }
|
69
|
+
return { tostring(count), allowed }
|
63
70
|
LUA
|
64
71
|
)
|
65
72
|
|
@@ -69,17 +76,17 @@ module Berater
|
|
69
76
|
|
70
77
|
count, allowed = LUA_SCRIPT.eval(
|
71
78
|
redis,
|
72
|
-
[ cache_key(key)
|
79
|
+
[ cache_key(key) ],
|
73
80
|
[ ts, capacity, @interval_msec, cost ]
|
74
81
|
)
|
75
82
|
|
76
|
-
|
83
|
+
count = count.include?('.') ? count.to_f : count.to_i
|
84
|
+
|
85
|
+
raise Overloaded unless allowed
|
77
86
|
|
78
87
|
Lock.new(capacity, count)
|
79
88
|
end
|
80
89
|
|
81
|
-
alias overrated? overloaded?
|
82
|
-
|
83
90
|
def to_s
|
84
91
|
msg = if interval.is_a? Numeric
|
85
92
|
if interval == 1
|
@@ -1,10 +1,6 @@
|
|
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
|
@@ -12,13 +8,13 @@ module Berater
|
|
12
8
|
def matches?(obj)
|
13
9
|
case obj
|
14
10
|
when Proc
|
15
|
-
# eg. expect { ... }.to
|
11
|
+
# eg. expect { ... }.to be_overloaded
|
16
12
|
res = obj.call
|
17
13
|
|
18
14
|
if res.is_a? Berater::Limiter
|
19
15
|
# eg. expect { Berater.new(...) }.to be_overloaded
|
20
16
|
@limiter = res
|
21
|
-
|
17
|
+
@limiter.utilization >= 1
|
22
18
|
else
|
23
19
|
# eg. expect { Berater(...) }.to be_overloaded
|
24
20
|
# eg. expect { limiter.limit }.to be_overloaded
|
@@ -27,55 +23,39 @@ module Berater
|
|
27
23
|
when Berater::Limiter
|
28
24
|
# eg. expect(Berater.new(...)).to be_overloaded
|
29
25
|
@limiter = obj
|
30
|
-
|
26
|
+
@limiter.utilization >= 1
|
31
27
|
end
|
32
|
-
rescue
|
28
|
+
rescue Berater::Overloaded
|
33
29
|
true
|
34
30
|
end
|
35
31
|
|
36
32
|
def description
|
37
33
|
if @limiter
|
38
|
-
"be
|
34
|
+
"be overloaded"
|
39
35
|
else
|
40
|
-
"raise #{
|
36
|
+
"raise #{Berater::Overloaded}"
|
41
37
|
end
|
42
38
|
end
|
43
39
|
|
44
40
|
def failure_message
|
45
41
|
if @limiter
|
46
|
-
"expected to be
|
42
|
+
"expected to be overloaded"
|
47
43
|
else
|
48
|
-
"expected #{
|
44
|
+
"expected #{Berater::Overloaded} to be raised"
|
49
45
|
end
|
50
46
|
end
|
51
47
|
|
52
48
|
def failure_message_when_negated
|
53
49
|
if @limiter
|
54
|
-
"expected not to be
|
50
|
+
"expected not to be overloaded"
|
55
51
|
else
|
56
|
-
"did not expect #{
|
52
|
+
"did not expect #{Berater::Overloaded} to be raised"
|
57
53
|
end
|
58
54
|
end
|
59
|
-
|
60
|
-
private def verb
|
61
|
-
@type.to_s.split('::')[-1].downcase
|
62
|
-
end
|
63
55
|
end
|
64
56
|
|
65
57
|
def be_overloaded
|
66
|
-
Overloaded.new
|
67
|
-
end
|
68
|
-
|
69
|
-
def be_overrated
|
70
|
-
Overloaded.new(Berater::RateLimiter::Overrated)
|
71
|
-
end
|
72
|
-
|
73
|
-
def be_incapacitated
|
74
|
-
Overloaded.new(Berater::ConcurrencyLimiter::Incapacitated)
|
75
|
-
end
|
76
|
-
|
77
|
-
def be_inhibited
|
78
|
-
Overloaded.new(Berater::Inhibitor::Inhibited)
|
58
|
+
Overloaded.new
|
79
59
|
end
|
80
60
|
end
|
81
61
|
end
|
data/lib/berater/test_mode.rb
CHANGED
@@ -19,14 +19,7 @@ module Berater
|
|
19
19
|
when :pass
|
20
20
|
Lock.new(Float::INFINITY, 0)
|
21
21
|
when :fail
|
22
|
-
|
23
|
-
e = self.class.constants.map do |name|
|
24
|
-
self.class.const_get(name)
|
25
|
-
end.find do |const|
|
26
|
-
const.is_a?(Class) && const < Berater::Overloaded
|
27
|
-
end || Berater::Overloaded
|
28
|
-
|
29
|
-
raise e
|
22
|
+
raise Overloaded
|
30
23
|
else
|
31
24
|
super
|
32
25
|
end
|
data/lib/berater/version.rb
CHANGED
data/spec/berater_spec.rb
CHANGED
@@ -25,7 +25,7 @@ describe Berater do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
describe '.new' do
|
28
|
-
context '
|
28
|
+
context 'Unlimiter mode' do
|
29
29
|
let(:limiter) { Berater.new(:key, Float::INFINITY) }
|
30
30
|
|
31
31
|
it 'instantiates an Unlimiter' do
|
@@ -44,7 +44,7 @@ describe Berater do
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
context '
|
47
|
+
context 'Inhibitor mode' do
|
48
48
|
let(:limiter) { Berater.new(:key, 0) }
|
49
49
|
|
50
50
|
it 'instantiates an Inhibitor' do
|
@@ -64,7 +64,7 @@ describe Berater do
|
|
64
64
|
end
|
65
65
|
|
66
66
|
context 'rate mode' do
|
67
|
-
let(:limiter) { Berater.new(:key, 1, :second) }
|
67
|
+
let(:limiter) { Berater.new(:key, 1, interval: :second) }
|
68
68
|
|
69
69
|
it 'instantiates a RateLimiter' do
|
70
70
|
expect(limiter).to be_a Berater::RateLimiter
|
@@ -77,7 +77,7 @@ describe Berater do
|
|
77
77
|
|
78
78
|
it 'accepts options' do
|
79
79
|
redis = double('Redis')
|
80
|
-
limiter = Berater.new(:key, 1, :second, redis: redis)
|
80
|
+
limiter = Berater.new(:key, 1, interval: :second, redis: redis)
|
81
81
|
expect(limiter.redis).to be redis
|
82
82
|
end
|
83
83
|
end
|
@@ -103,20 +103,20 @@ describe Berater do
|
|
103
103
|
end
|
104
104
|
|
105
105
|
describe 'Berater() - convenience method' do
|
106
|
-
RSpec.shared_examples 'test convenience' do |klass,
|
106
|
+
RSpec.shared_examples 'test convenience' do |klass, capacity, **opts|
|
107
107
|
it 'creates a limiter' do
|
108
|
-
limiter = Berater(:key,
|
108
|
+
limiter = Berater(:key, capacity, **opts)
|
109
109
|
expect(limiter).to be_a klass
|
110
110
|
end
|
111
111
|
|
112
112
|
context 'with a block' do
|
113
113
|
it 'creates a limiter and calls limit' do
|
114
|
-
limiter = Berater(:key,
|
114
|
+
limiter = Berater(:key, capacity, **opts)
|
115
115
|
expect(klass).to receive(:new).and_return(limiter)
|
116
116
|
expect(limiter).to receive(:limit).and_call_original
|
117
117
|
|
118
118
|
begin
|
119
|
-
res = Berater(:key,
|
119
|
+
res = Berater(:key, capacity, **opts) { true }
|
120
120
|
expect(res).to be true
|
121
121
|
rescue Berater::Overloaded
|
122
122
|
expect(klass).to be Berater::Inhibitor
|
@@ -127,7 +127,7 @@ describe Berater do
|
|
127
127
|
|
128
128
|
include_examples 'test convenience', Berater::Unlimiter, Float::INFINITY
|
129
129
|
include_examples 'test convenience', Berater::Inhibitor, 0
|
130
|
-
include_examples 'test convenience', Berater::RateLimiter, 1, :second
|
130
|
+
include_examples 'test convenience', Berater::RateLimiter, 1, interval: :second
|
131
131
|
include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
|
132
132
|
end
|
133
133
|
|