berater 0.1.0 → 0.2.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 +17 -24
- data/lib/berater/base_limiter.rb +11 -19
- data/lib/berater/concurrency_limiter.rb +28 -88
- data/lib/berater/inhibitor.rb +3 -9
- data/lib/berater/lock.rb +36 -0
- data/lib/berater/rate_limiter.rb +14 -16
- data/lib/berater/unlimiter.rb +14 -9
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +31 -98
- data/spec/concurrency_limiter_spec.rb +58 -73
- data/spec/concurrency_lock_spec.rb +26 -21
- data/spec/inhibitor_spec.rb +3 -15
- data/spec/matcher_spec.rb +4 -4
- data/spec/rate_limiter_spec.rb +27 -50
- data/spec/rate_lock_spec.rb +20 -0
- data/spec/unlimiter_spec.rb +7 -31
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dec8c5d6d428f795d1489dace166451e2a3a42592d00d71155ed5cd0d5aaa909
|
4
|
+
data.tar.gz: 6956b6c2804b6d616074e239439d4f2a8e551e900db7801226948b9eb764d0fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 327cd2f3a7430c2b81bd1988abd636bfc7d97048c762d3807555e9c1196153570dbe3045fae813852a907ec00b89b8d2c36e9269ed0562419c8d98dd6c1d2b26
|
7
|
+
data.tar.gz: ea1cb39e9201091e4a1357e888ac36f630c326a271e3cfcafa13298b8f54424f776f4dd488e046e2178905d0df9fb5c3b1f0d454e331823cafe5dd338fe9ea6c
|
data/lib/berater.rb
CHANGED
@@ -1,53 +1,34 @@
|
|
1
|
-
require 'berater/base_limiter'
|
2
|
-
require 'berater/concurrency_limiter'
|
3
|
-
require 'berater/inhibitor'
|
4
|
-
require 'berater/rate_limiter'
|
5
|
-
require 'berater/unlimiter'
|
6
1
|
require 'berater/version'
|
2
|
+
require 'berater/lock'
|
7
3
|
|
8
4
|
|
9
5
|
module Berater
|
10
6
|
extend self
|
11
7
|
|
12
|
-
Overloaded
|
8
|
+
class Overloaded < StandardError; end
|
13
9
|
|
14
10
|
MODES = {}
|
15
11
|
|
16
|
-
attr_accessor :redis
|
12
|
+
attr_accessor :redis
|
17
13
|
|
18
14
|
def configure
|
19
|
-
self.mode = :unlimited # default
|
20
|
-
|
21
15
|
yield self
|
22
16
|
end
|
23
17
|
|
24
|
-
def new(mode, *args, **opts)
|
18
|
+
def new(key, mode, *args, **opts)
|
25
19
|
klass = MODES[mode.to_sym]
|
26
20
|
|
27
21
|
unless klass
|
28
22
|
raise ArgumentError, "invalid mode: #{mode}"
|
29
23
|
end
|
30
24
|
|
31
|
-
klass.new(*args, **opts)
|
25
|
+
klass.new(key, *args, **opts)
|
32
26
|
end
|
33
27
|
|
34
28
|
def register(mode, klass)
|
35
29
|
MODES[mode.to_sym] = klass
|
36
30
|
end
|
37
31
|
|
38
|
-
def mode=(mode)
|
39
|
-
unless MODES.include? mode.to_sym
|
40
|
-
raise ArgumentError, "invalid mode: #{mode}"
|
41
|
-
end
|
42
|
-
|
43
|
-
@mode = mode.to_sym
|
44
|
-
end
|
45
|
-
|
46
|
-
def limit(*args, **opts, &block)
|
47
|
-
mode = opts.delete(:mode) { self.mode }
|
48
|
-
new(mode, *args, **opts).limit(&block)
|
49
|
-
end
|
50
|
-
|
51
32
|
def expunge
|
52
33
|
redis.scan_each(match: "#{self.name}*") do |key|
|
53
34
|
redis.del key
|
@@ -56,6 +37,18 @@ module Berater
|
|
56
37
|
|
57
38
|
end
|
58
39
|
|
40
|
+
# convenience method
|
41
|
+
def Berater(key, mode, *args, **opts, &block)
|
42
|
+
Berater.new(key, mode, *args, **opts).limit(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
# load and register limiters
|
46
|
+
require 'berater/base_limiter'
|
47
|
+
require 'berater/concurrency_limiter'
|
48
|
+
require 'berater/inhibitor'
|
49
|
+
require 'berater/rate_limiter'
|
50
|
+
require 'berater/unlimiter'
|
51
|
+
|
59
52
|
Berater.register(:concurrency, Berater::ConcurrencyLimiter)
|
60
53
|
Berater.register(:inhibited, Berater::Inhibitor)
|
61
54
|
Berater.register(:rate, Berater::RateLimiter)
|
data/lib/berater/base_limiter.rb
CHANGED
@@ -1,33 +1,25 @@
|
|
1
1
|
module Berater
|
2
2
|
class BaseLimiter
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
attr_reader :options
|
7
|
-
|
8
|
-
def initialize(**opts)
|
9
|
-
@options = opts
|
10
|
-
end
|
11
|
-
|
12
|
-
def key
|
13
|
-
if options[:key]
|
14
|
-
"#{self.class}:#{options[:key]}"
|
15
|
-
else
|
16
|
-
# default value
|
17
|
-
self.class.to_s
|
18
|
-
end
|
19
|
-
end
|
4
|
+
attr_reader :key, :options
|
20
5
|
|
21
6
|
def redis
|
22
7
|
options[:redis] || Berater.redis
|
23
8
|
end
|
24
9
|
|
25
|
-
def limit
|
10
|
+
def limit
|
26
11
|
raise NotImplementedError
|
27
12
|
end
|
28
13
|
|
29
|
-
|
30
|
-
|
14
|
+
protected
|
15
|
+
|
16
|
+
def initialize(key, **opts)
|
17
|
+
@key = key
|
18
|
+
@options = opts
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache_key(key)
|
22
|
+
"#{self.class}:#{key}"
|
31
23
|
end
|
32
24
|
|
33
25
|
end
|
@@ -5,14 +5,14 @@ module Berater
|
|
5
5
|
|
6
6
|
attr_reader :capacity, :timeout
|
7
7
|
|
8
|
-
def initialize(capacity, **opts)
|
9
|
-
super(**opts)
|
8
|
+
def initialize(key, capacity, **opts)
|
9
|
+
super(key, **opts)
|
10
10
|
|
11
11
|
self.capacity = capacity
|
12
12
|
self.timeout = opts[:timeout] || 0
|
13
13
|
end
|
14
14
|
|
15
|
-
def capacity=(capacity)
|
15
|
+
private def capacity=(capacity)
|
16
16
|
unless capacity.is_a? Integer
|
17
17
|
raise ArgumentError, "expected Integer, found #{capacity.class}"
|
18
18
|
end
|
@@ -22,7 +22,7 @@ module Berater
|
|
22
22
|
@capacity = capacity
|
23
23
|
end
|
24
24
|
|
25
|
-
def timeout=(timeout)
|
25
|
+
private def timeout=(timeout)
|
26
26
|
unless timeout.is_a? Integer
|
27
27
|
raise ArgumentError, "expected Integer, found #{timeout.class}"
|
28
28
|
end
|
@@ -32,117 +32,57 @@ module Berater
|
|
32
32
|
@timeout = timeout
|
33
33
|
end
|
34
34
|
|
35
|
-
class Lock
|
36
|
-
attr_reader :limiter, :id
|
37
|
-
|
38
|
-
def initialize(limiter, id)
|
39
|
-
@limiter = limiter
|
40
|
-
@id = id
|
41
|
-
@released = false
|
42
|
-
@locked_at = Time.now
|
43
|
-
end
|
44
|
-
|
45
|
-
def release
|
46
|
-
raise 'lock already released' if released?
|
47
|
-
raise 'lock expired' if expired?
|
48
|
-
|
49
|
-
@released = limiter.release(self)
|
50
|
-
end
|
51
|
-
|
52
|
-
def released?
|
53
|
-
@released
|
54
|
-
end
|
55
|
-
|
56
|
-
def expired?
|
57
|
-
@locked_at + limiter.timeout < Time.now
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
35
|
LUA_SCRIPT = <<~LUA.gsub(/^\s*(--.*\n)?/, '')
|
62
36
|
local key = KEYS[1]
|
37
|
+
local lock_key = KEYS[2]
|
63
38
|
local capacity = tonumber(ARGV[1])
|
64
|
-
local
|
65
|
-
|
66
|
-
local exists
|
67
|
-
local count
|
39
|
+
local ts = tonumber(ARGV[2])
|
40
|
+
local ttl = tonumber(ARGV[3])
|
68
41
|
local lock
|
69
|
-
local ts = unpack(redis.call('TIME'))
|
70
42
|
|
71
|
-
--
|
72
|
-
if ttl
|
73
|
-
|
74
|
-
else
|
75
|
-
-- and refresh TTL while we're at it
|
76
|
-
exists = redis.call('EXPIRE', key, ttl * 2)
|
43
|
+
-- purge stale hosts
|
44
|
+
if ttl > 0 then
|
45
|
+
redis.call('ZREMRANGEBYSCORE', key, '-inf', ts - ttl)
|
77
46
|
end
|
78
47
|
|
79
|
-
|
80
|
-
|
81
|
-
if ttl > 0 then
|
82
|
-
redis.call('ZREMRANGEBYSCORE', key, '-inf', ts - ttl)
|
83
|
-
end
|
84
|
-
|
85
|
-
-- check capacity (subtract one for next lock entry)
|
86
|
-
count = redis.call('ZCARD', key) - 1
|
87
|
-
|
88
|
-
if count < capacity then
|
89
|
-
-- yay, grab a lock
|
90
|
-
|
91
|
-
-- regenerate next lock entry, which has score inf
|
92
|
-
lock = unpack(redis.call('ZPOPMAX', key))
|
93
|
-
redis.call('ZADD', key, 'inf', (lock + 1) % 2^52)
|
94
|
-
|
95
|
-
count = count + 1
|
96
|
-
end
|
97
|
-
else
|
98
|
-
count = 1
|
99
|
-
lock = "1"
|
48
|
+
-- check capacity
|
49
|
+
local count = redis.call('ZCARD', key)
|
100
50
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
if ttl > 0 then
|
105
|
-
redis.call('EXPIRE', key, ttl * 2)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
if lock then
|
110
|
-
-- store lock and timestamp
|
51
|
+
if count < capacity then
|
52
|
+
-- grab a lock
|
53
|
+
lock = redis.call('INCR', lock_key)
|
111
54
|
redis.call('ZADD', key, ts, lock)
|
55
|
+
count = count + 1
|
112
56
|
end
|
113
57
|
|
114
58
|
return { count, lock }
|
115
59
|
LUA
|
116
60
|
|
117
|
-
def limit
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
).limit(&block)
|
124
|
-
end
|
125
|
-
|
126
|
-
count, lock_id = redis.eval(LUA_SCRIPT, [ key ], [ capacity, timeout ])
|
61
|
+
def limit
|
62
|
+
count, lock_id = redis.eval(
|
63
|
+
LUA_SCRIPT,
|
64
|
+
[ cache_key(key), cache_key('lock_id') ],
|
65
|
+
[ capacity, Time.now.to_i, timeout ]
|
66
|
+
)
|
127
67
|
|
128
68
|
raise Incapacitated unless lock_id
|
129
69
|
|
130
|
-
lock = Lock.new(self, lock_id)
|
70
|
+
lock = Lock.new(self, lock_id, count, -> { release(lock_id) })
|
131
71
|
|
132
72
|
if block_given?
|
133
73
|
begin
|
134
|
-
yield
|
74
|
+
yield lock
|
135
75
|
ensure
|
136
|
-
release
|
76
|
+
lock.release
|
137
77
|
end
|
138
78
|
else
|
139
79
|
lock
|
140
80
|
end
|
141
81
|
end
|
142
82
|
|
143
|
-
def release(
|
144
|
-
res = redis.zrem(key,
|
145
|
-
res == true || res == 1
|
83
|
+
private def release(lock_id)
|
84
|
+
res = redis.zrem(cache_key(key), lock_id)
|
85
|
+
res == true || res == 1 # depending on which version of Redis
|
146
86
|
end
|
147
87
|
|
148
88
|
end
|
data/lib/berater/inhibitor.rb
CHANGED
@@ -3,17 +3,11 @@ module Berater
|
|
3
3
|
|
4
4
|
class Inhibited < Overloaded; end
|
5
5
|
|
6
|
-
def initialize(*args, **opts)
|
7
|
-
super(**opts)
|
6
|
+
def initialize(key = :inhibitor, *args, **opts)
|
7
|
+
super(key, **opts)
|
8
8
|
end
|
9
9
|
|
10
|
-
def limit
|
11
|
-
unless opts.empty?
|
12
|
-
return self.class.new(
|
13
|
-
**options.merge(opts)
|
14
|
-
).limit(&block)
|
15
|
-
end
|
16
|
-
|
10
|
+
def limit
|
17
11
|
raise Inhibited
|
18
12
|
end
|
19
13
|
|
data/lib/berater/lock.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Berater
|
2
|
+
class Lock
|
3
|
+
|
4
|
+
attr_reader :limiter, :id, :contention
|
5
|
+
|
6
|
+
def initialize(limiter, id, contention, release_fn = nil)
|
7
|
+
@limiter = limiter
|
8
|
+
@id = id
|
9
|
+
@contention = contention
|
10
|
+
@locked_at = Time.now
|
11
|
+
@release_fn = release_fn
|
12
|
+
@released_at = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def locked?
|
16
|
+
@released_at.nil? && !expired?
|
17
|
+
end
|
18
|
+
|
19
|
+
def expired?
|
20
|
+
timeout > 0 && @locked_at + timeout < Time.now
|
21
|
+
end
|
22
|
+
|
23
|
+
def release
|
24
|
+
raise 'lock expired' if expired?
|
25
|
+
raise 'lock already released' unless locked?
|
26
|
+
|
27
|
+
@released_at = Time.now
|
28
|
+
@release_fn ? @release_fn.call : true
|
29
|
+
end
|
30
|
+
|
31
|
+
private def timeout
|
32
|
+
limiter.respond_to?(:timeout) ? limiter.timeout : 0
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/lib/berater/rate_limiter.rb
CHANGED
@@ -5,14 +5,14 @@ module Berater
|
|
5
5
|
|
6
6
|
attr_accessor :count, :interval
|
7
7
|
|
8
|
-
def initialize(count, interval, **opts)
|
9
|
-
super(**opts)
|
8
|
+
def initialize(key, count, interval, **opts)
|
9
|
+
super(key, **opts)
|
10
10
|
|
11
11
|
self.count = count
|
12
12
|
self.interval = interval
|
13
13
|
end
|
14
14
|
|
15
|
-
def count=(count)
|
15
|
+
private def count=(count)
|
16
16
|
unless count.is_a? Integer
|
17
17
|
raise ArgumentError, "expected Integer, found #{count.class}"
|
18
18
|
end
|
@@ -22,7 +22,7 @@ module Berater
|
|
22
22
|
@count = count
|
23
23
|
end
|
24
24
|
|
25
|
-
def interval=(interval)
|
25
|
+
private def interval=(interval)
|
26
26
|
@interval = interval.dup
|
27
27
|
|
28
28
|
case @interval
|
@@ -51,19 +51,11 @@ module Berater
|
|
51
51
|
@interval
|
52
52
|
end
|
53
53
|
|
54
|
-
def limit
|
55
|
-
unless opts.empty?
|
56
|
-
return self.class.new(
|
57
|
-
count,
|
58
|
-
interval,
|
59
|
-
options.merge(opts)
|
60
|
-
).limit(&block)
|
61
|
-
end
|
62
|
-
|
54
|
+
def limit
|
63
55
|
ts = Time.now.to_i
|
64
56
|
|
65
57
|
# bucket into time slot
|
66
|
-
rkey = "%s:%d" % [ key, ts - ts % @interval ]
|
58
|
+
rkey = "%s:%d" % [ cache_key(key), ts - ts % @interval ]
|
67
59
|
|
68
60
|
count, _ = redis.multi do
|
69
61
|
redis.incr rkey
|
@@ -72,10 +64,16 @@ module Berater
|
|
72
64
|
|
73
65
|
raise Overrated if count > @count
|
74
66
|
|
67
|
+
lock = Lock.new(self, count, count)
|
68
|
+
|
75
69
|
if block_given?
|
76
|
-
|
70
|
+
begin
|
71
|
+
yield lock
|
72
|
+
ensure
|
73
|
+
lock.release
|
74
|
+
end
|
77
75
|
else
|
78
|
-
|
76
|
+
lock
|
79
77
|
end
|
80
78
|
end
|
81
79
|
|
data/lib/berater/unlimiter.rb
CHANGED
@@ -1,18 +1,23 @@
|
|
1
1
|
module Berater
|
2
2
|
class Unlimiter < BaseLimiter
|
3
3
|
|
4
|
-
def initialize(*args, **opts)
|
5
|
-
super(**opts)
|
4
|
+
def initialize(key = :unlimiter, *args, **opts)
|
5
|
+
super(key, **opts)
|
6
6
|
end
|
7
7
|
|
8
|
-
def limit
|
9
|
-
|
10
|
-
|
11
|
-
**options.merge(opts)
|
12
|
-
).limit(&block)
|
13
|
-
end
|
8
|
+
def limit
|
9
|
+
count = redis.incr(cache_key('count'))
|
10
|
+
lock = Lock.new(self, count, count)
|
14
11
|
|
15
|
-
|
12
|
+
if block_given?
|
13
|
+
begin
|
14
|
+
yield lock
|
15
|
+
ensure
|
16
|
+
lock.release
|
17
|
+
end
|
18
|
+
else
|
19
|
+
lock
|
20
|
+
end
|
16
21
|
end
|
17
22
|
|
18
23
|
end
|