berater 0.4.0 → 0.5.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 +27 -38
- data/lib/berater/concurrency_limiter.rb +58 -44
- data/lib/berater/dsl.rb +20 -9
- data/lib/berater/inhibitor.rb +7 -2
- data/lib/berater/limiter.rb +48 -2
- data/lib/berater/lock.rb +1 -10
- data/lib/berater/lua_script.rb +55 -0
- data/lib/berater/rate_limiter.rb +42 -73
- data/lib/berater/rspec.rb +2 -0
- data/lib/berater/rspec/matchers.rb +8 -6
- data/lib/berater/test_mode.rb +14 -5
- data/lib/berater/unlimiter.rb +6 -12
- data/lib/berater/utils.rb +46 -0
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +33 -70
- data/spec/concurrency_limiter_spec.rb +138 -63
- data/spec/dsl_refinement_spec.rb +46 -0
- data/spec/dsl_spec.rb +72 -0
- data/spec/inhibitor_spec.rb +2 -4
- data/spec/limiter_spec.rb +71 -0
- data/spec/lua_script_spec.rb +97 -0
- data/spec/matchers_spec.rb +14 -2
- data/spec/rate_limiter_spec.rb +94 -97
- data/spec/riddle_spec.rb +102 -0
- data/spec/test_mode_spec.rb +108 -78
- data/spec/unlimiter_spec.rb +3 -9
- data/spec/utils_spec.rb +78 -0
- metadata +31 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa5d51d6eea9d4691e58e8dd7065a1d596a57abdac1227fa19bd22cd2f42c15d
|
4
|
+
data.tar.gz: 55ac15bf6f2f295c525fdea57b3af9b518935a12dd4b644e3c0c9c085429ca50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52df5781f9c37c7f1c9dde09e579c8da1f2d62f405835b771c6087cda8af046018e85a88629f7c5d3a2be9aafd2680e348d1b2f91bc63fe456e6e51ec5d0fd02
|
7
|
+
data.tar.gz: '0928cb6f2e8e07810dec96f5af302ff9f62d519b9f19ef6f28acb5621569f13edb6d784cbd7084b738df8dfc232ace26e3ce0c6c4a7f2ac3e4391c97789bafae'
|
data/lib/berater.rb
CHANGED
@@ -1,48 +1,40 @@
|
|
1
|
-
require 'berater/
|
1
|
+
require 'berater/limiter'
|
2
2
|
require 'berater/lock'
|
3
|
-
|
3
|
+
require 'berater/lua_script'
|
4
|
+
require 'berater/utils'
|
5
|
+
require 'berater/version'
|
4
6
|
|
5
7
|
module Berater
|
6
8
|
extend self
|
7
9
|
|
8
10
|
class Overloaded < StandardError; end
|
9
11
|
|
10
|
-
MODES = {}
|
11
|
-
|
12
12
|
attr_accessor :redis
|
13
13
|
|
14
14
|
def configure
|
15
15
|
yield self
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
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
|
18
|
+
def reset
|
19
|
+
@redis = nil
|
20
|
+
end
|
27
21
|
|
28
|
-
|
22
|
+
def new(key, capacity, interval = nil, **opts)
|
23
|
+
case capacity
|
24
|
+
when :unlimited, Float::INFINITY
|
25
|
+
Berater::Unlimiter
|
26
|
+
when :inhibited, 0
|
27
|
+
Berater::Inhibitor
|
29
28
|
else
|
30
|
-
if
|
31
|
-
|
29
|
+
if interval
|
30
|
+
Berater::RateLimiter
|
31
|
+
else
|
32
|
+
Berater::ConcurrencyLimiter
|
32
33
|
end
|
34
|
+
end.yield_self do |klass|
|
35
|
+
args = [ key, capacity, interval ].compact
|
36
|
+
klass.new(*args, **opts)
|
33
37
|
end
|
34
|
-
|
35
|
-
klass = MODES[mode.to_sym]
|
36
|
-
|
37
|
-
unless klass
|
38
|
-
raise ArgumentError, "invalid mode: #{mode}"
|
39
|
-
end
|
40
|
-
|
41
|
-
klass.new(key, *args, **opts)
|
42
|
-
end
|
43
|
-
|
44
|
-
def register(mode, klass)
|
45
|
-
MODES[mode.to_sym] = klass
|
46
38
|
end
|
47
39
|
|
48
40
|
def expunge
|
@@ -54,20 +46,17 @@ module Berater
|
|
54
46
|
end
|
55
47
|
|
56
48
|
# convenience method
|
57
|
-
def Berater(key,
|
58
|
-
Berater.new(key,
|
49
|
+
def Berater(key, capacity, interval = nil, **opts, &block)
|
50
|
+
limiter = Berater.new(key, capacity, interval, **opts)
|
51
|
+
if block_given?
|
52
|
+
limiter.limit(&block)
|
53
|
+
else
|
54
|
+
limiter
|
55
|
+
end
|
59
56
|
end
|
60
57
|
|
61
58
|
# load limiters
|
62
|
-
require 'berater/limiter'
|
63
59
|
require 'berater/concurrency_limiter'
|
64
60
|
require 'berater/inhibitor'
|
65
61
|
require 'berater/rate_limiter'
|
66
62
|
require 'berater/unlimiter'
|
67
|
-
|
68
|
-
Berater.register(:concurrency, Berater::ConcurrencyLimiter)
|
69
|
-
Berater.register(:inhibited, Berater::Inhibitor)
|
70
|
-
Berater.register(:rate, Berater::RateLimiter)
|
71
|
-
Berater.register(:unlimited, Berater::Unlimiter)
|
72
|
-
|
73
|
-
require 'berater/dsl'
|
@@ -3,42 +3,28 @@ module Berater
|
|
3
3
|
|
4
4
|
class Incapacitated < Overloaded; end
|
5
5
|
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :timeout
|
7
7
|
|
8
8
|
def initialize(key, capacity, **opts)
|
9
|
-
super(key, **opts)
|
9
|
+
super(key, capacity, **opts)
|
10
10
|
|
11
|
-
self.capacity = capacity
|
12
11
|
self.timeout = opts[:timeout] || 0
|
13
12
|
end
|
14
13
|
|
15
|
-
private def capacity=(capacity)
|
16
|
-
unless capacity.is_a? Integer
|
17
|
-
raise ArgumentError, "expected Integer, found #{capacity.class}"
|
18
|
-
end
|
19
|
-
|
20
|
-
raise ArgumentError, "capacity must be >= 0" unless capacity >= 0
|
21
|
-
|
22
|
-
@capacity = capacity
|
23
|
-
end
|
24
|
-
|
25
14
|
private def timeout=(timeout)
|
26
|
-
unless timeout.is_a? Integer
|
27
|
-
raise ArgumentError, "expected Integer, found #{timeout.class}"
|
28
|
-
end
|
29
|
-
|
30
|
-
raise ArgumentError, "timeout must be >= 0" unless timeout >= 0
|
31
|
-
|
32
15
|
@timeout = timeout
|
16
|
+
timeout = 0 if timeout == Float::INFINITY
|
17
|
+
@timeout_usec = Berater::Utils.to_usec(timeout)
|
33
18
|
end
|
34
19
|
|
35
|
-
LUA_SCRIPT = <<~LUA
|
20
|
+
LUA_SCRIPT = Berater::LuaScript(<<~LUA
|
36
21
|
local key = KEYS[1]
|
37
22
|
local lock_key = KEYS[2]
|
38
23
|
local capacity = tonumber(ARGV[1])
|
39
24
|
local ts = tonumber(ARGV[2])
|
40
25
|
local ttl = tonumber(ARGV[3])
|
41
|
-
local
|
26
|
+
local cost = tonumber(ARGV[4])
|
27
|
+
local lock_ids = {}
|
42
28
|
|
43
29
|
-- purge stale hosts
|
44
30
|
if ttl > 0 then
|
@@ -48,41 +34,69 @@ module Berater
|
|
48
34
|
-- check capacity
|
49
35
|
local count = redis.call('ZCARD', key)
|
50
36
|
|
51
|
-
if
|
52
|
-
--
|
53
|
-
|
54
|
-
|
55
|
-
|
37
|
+
if cost == 0 then
|
38
|
+
-- just check limit, ie. for .overlimit?
|
39
|
+
if count < capacity then
|
40
|
+
table.insert(lock_ids, true)
|
41
|
+
end
|
42
|
+
elseif (count + cost) <= capacity then
|
43
|
+
-- grab locks, one per cost
|
44
|
+
local lock_id = redis.call('INCRBY', lock_key, cost)
|
45
|
+
local locks = {}
|
46
|
+
|
47
|
+
for i = lock_id - cost + 1, lock_id do
|
48
|
+
table.insert(lock_ids, i)
|
49
|
+
|
50
|
+
table.insert(locks, ts)
|
51
|
+
table.insert(locks, i)
|
52
|
+
end
|
53
|
+
|
54
|
+
redis.call('ZADD', key, unpack(locks))
|
55
|
+
count = count + cost
|
56
|
+
|
57
|
+
if ttl > 0 then
|
58
|
+
redis.call('PEXPIRE', key, ttl)
|
59
|
+
end
|
56
60
|
end
|
57
61
|
|
58
|
-
return { count,
|
62
|
+
return { count, unpack(lock_ids) }
|
59
63
|
LUA
|
64
|
+
)
|
65
|
+
|
66
|
+
def limit(capacity: nil, cost: 1, &block)
|
67
|
+
capacity ||= @capacity
|
68
|
+
# cost is Integer >= 0
|
69
|
+
|
70
|
+
# timestamp in microseconds
|
71
|
+
ts = (Time.now.to_f * 10**6).to_i
|
60
72
|
|
61
|
-
|
62
|
-
|
63
|
-
LUA_SCRIPT,
|
73
|
+
count, *lock_ids = LUA_SCRIPT.eval(
|
74
|
+
redis,
|
64
75
|
[ cache_key(key), cache_key('lock_id') ],
|
65
|
-
[ capacity,
|
76
|
+
[ capacity, ts, @timeout_usec, cost ]
|
66
77
|
)
|
67
78
|
|
68
|
-
raise Incapacitated
|
79
|
+
raise Incapacitated if lock_ids.empty?
|
69
80
|
|
70
|
-
|
71
|
-
|
72
|
-
if block_given?
|
73
|
-
begin
|
74
|
-
yield lock
|
75
|
-
ensure
|
76
|
-
lock.release
|
77
|
-
end
|
81
|
+
if cost == 0
|
82
|
+
lock = Lock.new(self, nil, count)
|
78
83
|
else
|
79
|
-
lock
|
84
|
+
lock = Lock.new(self, lock_ids[0], count, -> { release(lock_ids) })
|
80
85
|
end
|
86
|
+
|
87
|
+
yield_lock(lock, &block)
|
88
|
+
end
|
89
|
+
|
90
|
+
def overloaded?
|
91
|
+
limit(cost: 0) { false }
|
92
|
+
rescue Overloaded
|
93
|
+
true
|
81
94
|
end
|
95
|
+
alias incapacitated? overloaded?
|
82
96
|
|
83
|
-
private def release(
|
84
|
-
res = redis.zrem(cache_key(key),
|
85
|
-
res == true || res ==
|
97
|
+
private def release(lock_ids)
|
98
|
+
res = redis.zrem(cache_key(key), lock_ids)
|
99
|
+
res == true || res == lock_ids.count # depending on which version of Redis
|
86
100
|
end
|
87
101
|
|
88
102
|
def to_s
|
data/lib/berater/dsl.rb
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
module Berater
|
2
2
|
module DSL
|
3
|
+
refine Berater.singleton_class do
|
4
|
+
def new(key, mode = nil, *args, **opts, &block)
|
5
|
+
if mode.nil?
|
6
|
+
unless block_given?
|
7
|
+
raise ArgumentError, 'expected either mode or block'
|
8
|
+
end
|
9
|
+
|
10
|
+
mode, *args = DSL.eval(&block)
|
11
|
+
else
|
12
|
+
if block_given?
|
13
|
+
raise ArgumentError, 'expected either mode or block, not both'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
super(key, mode, *args, **opts)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
3
21
|
extend self
|
4
22
|
|
5
23
|
def eval &block
|
@@ -18,13 +36,6 @@ module Berater
|
|
18
36
|
|
19
37
|
private
|
20
38
|
|
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
39
|
KEYWORDS = [
|
29
40
|
:second, :minute, :hour,
|
30
41
|
:unlimited, :inhibited,
|
@@ -33,12 +44,12 @@ module Berater
|
|
33
44
|
def install
|
34
45
|
Integer.class_eval do
|
35
46
|
def per(unit)
|
36
|
-
[
|
47
|
+
[ self, unit ]
|
37
48
|
end
|
38
49
|
alias every per
|
39
50
|
|
40
51
|
def at_once
|
41
|
-
[
|
52
|
+
[ self ]
|
42
53
|
end
|
43
54
|
alias concurrently at_once
|
44
55
|
alias at_a_time at_once
|
data/lib/berater/inhibitor.rb
CHANGED
@@ -4,12 +4,17 @@ module Berater
|
|
4
4
|
class Inhibited < Overloaded; end
|
5
5
|
|
6
6
|
def initialize(key = :inhibitor, *args, **opts)
|
7
|
-
super(key, **opts)
|
7
|
+
super(key, 0, **opts)
|
8
8
|
end
|
9
9
|
|
10
|
-
def limit
|
10
|
+
def limit(**opts)
|
11
11
|
raise Inhibited
|
12
12
|
end
|
13
13
|
|
14
|
+
def overloaded?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
alias inhibited? overloaded?
|
18
|
+
|
14
19
|
end
|
15
20
|
end
|
data/lib/berater/limiter.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Berater
|
2
2
|
class Limiter
|
3
3
|
|
4
|
-
attr_reader :key, :options
|
4
|
+
attr_reader :key, :capacity, :options
|
5
5
|
|
6
6
|
def redis
|
7
7
|
options[:redis] || Berater.redis
|
@@ -11,20 +11,66 @@ module Berater
|
|
11
11
|
raise NotImplementedError
|
12
12
|
end
|
13
13
|
|
14
|
+
def overloaded?
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
14
18
|
def to_s
|
15
19
|
"#<#{self.class}>"
|
16
20
|
end
|
17
21
|
|
22
|
+
def ==(other)
|
23
|
+
self.class == other.class &&
|
24
|
+
self.key == other.key &&
|
25
|
+
self.capacity == other.capacity &&
|
26
|
+
self.args == other.args &&
|
27
|
+
self.options == other.options &&
|
28
|
+
self.redis.connection == other.redis.connection
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.new(*)
|
32
|
+
# can only call via subclass
|
33
|
+
raise NotImplementedError if self == Berater::Limiter
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
18
38
|
protected
|
19
39
|
|
20
|
-
|
40
|
+
attr_reader :args
|
41
|
+
|
42
|
+
def initialize(key, capacity, *args, **opts)
|
21
43
|
@key = key
|
44
|
+
self.capacity = capacity
|
45
|
+
@args = args
|
22
46
|
@options = opts
|
23
47
|
end
|
24
48
|
|
49
|
+
def capacity=(capacity)
|
50
|
+
unless capacity.is_a?(Integer) || capacity == Float::INFINITY
|
51
|
+
raise ArgumentError, "expected Integer, found #{capacity.class}"
|
52
|
+
end
|
53
|
+
|
54
|
+
raise ArgumentError, "capacity must be >= 0" unless capacity >= 0
|
55
|
+
|
56
|
+
@capacity = capacity
|
57
|
+
end
|
58
|
+
|
25
59
|
def cache_key(key)
|
26
60
|
"#{self.class}:#{key}"
|
27
61
|
end
|
28
62
|
|
63
|
+
def yield_lock(lock, &block)
|
64
|
+
if block_given?
|
65
|
+
begin
|
66
|
+
yield lock
|
67
|
+
ensure
|
68
|
+
lock.release
|
69
|
+
end
|
70
|
+
else
|
71
|
+
lock
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
29
75
|
end
|
30
76
|
end
|
data/lib/berater/lock.rb
CHANGED
@@ -13,24 +13,15 @@ module Berater
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def locked?
|
16
|
-
@released_at.nil?
|
17
|
-
end
|
18
|
-
|
19
|
-
def expired?
|
20
|
-
timeout ? @locked_at + timeout < Time.now : false
|
16
|
+
@released_at.nil?
|
21
17
|
end
|
22
18
|
|
23
19
|
def release
|
24
|
-
raise 'lock expired' if expired?
|
25
20
|
raise 'lock already released' unless locked?
|
26
21
|
|
27
22
|
@released_at = Time.now
|
28
23
|
@release_fn ? @release_fn.call : true
|
29
24
|
end
|
30
25
|
|
31
|
-
def timeout
|
32
|
-
limiter.respond_to?(:timeout) ? limiter.timeout : nil
|
33
|
-
end
|
34
|
-
|
35
26
|
end
|
36
27
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Berater
|
4
|
+
class LuaScript
|
5
|
+
|
6
|
+
attr_reader :source
|
7
|
+
|
8
|
+
def initialize(source)
|
9
|
+
@source = source
|
10
|
+
end
|
11
|
+
|
12
|
+
def sha
|
13
|
+
@sha ||= Digest::SHA1.hexdigest(minify)
|
14
|
+
end
|
15
|
+
|
16
|
+
def eval(redis, *args)
|
17
|
+
redis.evalsha(sha, *args)
|
18
|
+
rescue Redis::CommandError => e
|
19
|
+
raise unless e.message.include?('NOSCRIPT')
|
20
|
+
|
21
|
+
# fall back to regular eval, which will trigger
|
22
|
+
# script to be cached for next time
|
23
|
+
redis.eval(minify, *args)
|
24
|
+
end
|
25
|
+
|
26
|
+
def load(redis)
|
27
|
+
redis.script(:load, minify).tap do |sha|
|
28
|
+
unless sha == self.sha
|
29
|
+
raise "unexpected script SHA: expected #{self.sha}, got #{sha}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def loaded?(redis)
|
35
|
+
redis.script(:exists, sha)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
source
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def minify
|
45
|
+
# trim comments (whole line and partial)
|
46
|
+
# and whitespace (prefix and empty lines)
|
47
|
+
@minify ||= source.gsub(/^\s*--.*\n|\s*--.*|^\s*|^$\n/, '').chomp
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def LuaScript(source)
|
53
|
+
LuaScript.new(source)
|
54
|
+
end
|
55
|
+
end
|