berater 0.2.0 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dec8c5d6d428f795d1489dace166451e2a3a42592d00d71155ed5cd0d5aaa909
4
- data.tar.gz: 6956b6c2804b6d616074e239439d4f2a8e551e900db7801226948b9eb764d0fc
3
+ metadata.gz: 5c5cc50ba02c18bf9ef9f6b9c1dd6ccb8f8af0297df95757ac13406dc4539c40
4
+ data.tar.gz: f80341dccff30ca3f344f3bdf21de44b1ef1715bfaf4140933a91252ddb7231e
5
5
  SHA512:
6
- metadata.gz: 327cd2f3a7430c2b81bd1988abd636bfc7d97048c762d3807555e9c1196153570dbe3045fae813852a907ec00b89b8d2c36e9269ed0562419c8d98dd6c1d2b26
7
- data.tar.gz: ea1cb39e9201091e4a1357e888ac36f630c326a271e3cfcafa13298b8f54424f776f4dd488e046e2178905d0df9fb5c3b1f0d454e331823cafe5dd338fe9ea6c
6
+ metadata.gz: ada3fffc841e71498f652dc990eed482bccc942b86280fc5ee736d49fdf99d8ab2f921b5b52d96f3588c268c34343669c00e0be8822c462a3e6f9dcf954a4005
7
+ data.tar.gz: 0363abef69cfdad8140d89373b5088cfa8a6259f16716011c82ceb1e2e9e948cdb4d44ddd36aaf1d05b121dc7d275d0ee34571a0a3ee9b964f4286375ec356cf
data/lib/berater.rb CHANGED
@@ -1,32 +1,40 @@
1
- require 'berater/version'
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 new(key, mode, *args, **opts)
19
- klass = MODES[mode.to_sym]
20
-
21
- unless klass
22
- raise ArgumentError, "invalid mode: #{mode}"
23
- end
24
-
25
- klass.new(key, *args, **opts)
18
+ def reset
19
+ @redis = nil
26
20
  end
27
21
 
28
- def register(mode, klass)
29
- MODES[mode.to_sym] = klass
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
28
+ else
29
+ if interval
30
+ Berater::RateLimiter
31
+ else
32
+ Berater::ConcurrencyLimiter
33
+ end
34
+ end.yield_self do |klass|
35
+ args = [ key, capacity, interval ].compact
36
+ klass.new(*args, **opts)
37
+ end
30
38
  end
31
39
 
32
40
  def expunge
@@ -38,18 +46,17 @@ module Berater
38
46
  end
39
47
 
40
48
  # convenience method
41
- def Berater(key, mode, *args, **opts, &block)
42
- Berater.new(key, mode, *args, **opts).limit(&block)
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
43
56
  end
44
57
 
45
- # load and register limiters
46
- require 'berater/base_limiter'
58
+ # load limiters
47
59
  require 'berater/concurrency_limiter'
48
60
  require 'berater/inhibitor'
49
61
  require 'berater/rate_limiter'
50
62
  require 'berater/unlimiter'
51
-
52
- Berater.register(:concurrency, Berater::ConcurrencyLimiter)
53
- Berater.register(:inhibited, Berater::Inhibitor)
54
- Berater.register(:rate, Berater::RateLimiter)
55
- Berater.register(:unlimited, Berater::Unlimiter)
@@ -1,44 +1,30 @@
1
1
  module Berater
2
- class ConcurrencyLimiter < BaseLimiter
2
+ class ConcurrencyLimiter < Limiter
3
3
 
4
4
  class Incapacitated < Overloaded; end
5
5
 
6
- attr_reader :capacity, :timeout
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_msec = Berater::Utils.to_msec(timeout)
33
18
  end
34
19
 
35
- LUA_SCRIPT = <<~LUA.gsub(/^\s*(--.*\n)?/, '')
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 lock
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,67 @@ module Berater
48
34
  -- check capacity
49
35
  local count = redis.call('ZCARD', key)
50
36
 
51
- if count < capacity then
52
- -- grab a lock
53
- lock = redis.call('INCR', lock_key)
54
- redis.call('ZADD', key, ts, lock)
55
- count = count + 1
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, lock }
62
+ return { count, unpack(lock_ids) }
59
63
  LUA
64
+ )
60
65
 
61
- def limit
62
- count, lock_id = redis.eval(
63
- LUA_SCRIPT,
66
+ protected def acquire_lock(capacity, cost)
67
+ # fractional cost is not supported, but make it work
68
+ capacity = capacity.to_i
69
+ cost = cost.ceil
70
+
71
+ # timestamp in milliseconds
72
+ ts = (Time.now.to_f * 10**3).to_i
73
+
74
+ count, *lock_ids = LUA_SCRIPT.eval(
75
+ redis,
64
76
  [ cache_key(key), cache_key('lock_id') ],
65
- [ capacity, Time.now.to_i, timeout ]
77
+ [ capacity, ts, @timeout_msec, cost ]
66
78
  )
67
79
 
68
- raise Incapacitated unless lock_id
69
-
70
- lock = Lock.new(self, lock_id, count, -> { release(lock_id) })
80
+ raise Incapacitated if lock_ids.empty?
71
81
 
72
- if block_given?
73
- begin
74
- yield lock
75
- ensure
76
- lock.release
77
- end
78
- else
79
- lock
82
+ release_fn = if cost > 0
83
+ proc { release(lock_ids) }
80
84
  end
85
+
86
+ Lock.new(capacity, count, release_fn)
87
+ end
88
+
89
+ alias incapacitated? overloaded?
90
+
91
+ private def release(lock_ids)
92
+ res = redis.zrem(cache_key(key), lock_ids)
93
+ res == true || res == lock_ids.count # depending on which version of Redis
81
94
  end
82
95
 
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
96
+ def to_s
97
+ "#<#{self.class}(#{key}: #{capacity} at a time)>"
86
98
  end
87
99
 
88
100
  end
@@ -0,0 +1,68 @@
1
+ module Berater
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
+
21
+ extend self
22
+
23
+ def eval &block
24
+ @keywords ||= Class.new do
25
+ # create a class where DSL keywords are methods
26
+ KEYWORDS.each do |keyword|
27
+ define_singleton_method(keyword) { keyword }
28
+ end
29
+ end
30
+
31
+ install
32
+ @keywords.class_eval &block
33
+ ensure
34
+ uninstall
35
+ end
36
+
37
+ private
38
+
39
+ KEYWORDS = [
40
+ :second, :minute, :hour,
41
+ :unlimited, :inhibited,
42
+ ].freeze
43
+
44
+ def install
45
+ Integer.class_eval do
46
+ def per(unit)
47
+ [ self, unit ]
48
+ end
49
+ alias every per
50
+
51
+ def at_once
52
+ [ self ]
53
+ end
54
+ alias concurrently at_once
55
+ alias at_a_time at_once
56
+ end
57
+ end
58
+
59
+ def uninstall
60
+ Integer.remove_method :per
61
+ Integer.remove_method :every
62
+
63
+ Integer.remove_method :at_once
64
+ Integer.remove_method :concurrently
65
+ Integer.remove_method :at_a_time
66
+ end
67
+ end
68
+ end
@@ -1,13 +1,15 @@
1
1
  module Berater
2
- class Inhibitor < BaseLimiter
2
+ class Inhibitor < Limiter
3
3
 
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
+ alias inhibited? overloaded?
11
+
12
+ protected def acquire_lock(*)
11
13
  raise Inhibited
12
14
  end
13
15
 
@@ -0,0 +1,94 @@
1
+ module Berater
2
+ class Limiter
3
+
4
+ attr_reader :key, :capacity, :options
5
+
6
+ def redis
7
+ options[:redis] || Berater.redis
8
+ end
9
+
10
+ def limit(capacity: nil, cost: 1, &block)
11
+ capacity ||= @capacity
12
+
13
+ unless capacity.is_a?(Numeric)
14
+ raise ArgumentError, "invalid capacity: #{capacity}"
15
+ end
16
+
17
+ unless cost.is_a?(Numeric) && cost >= 0
18
+ raise ArgumentError, "invalid cost: #{cost}"
19
+ end
20
+
21
+ lock = acquire_lock(capacity, cost)
22
+
23
+ if block_given?
24
+ begin
25
+ yield lock
26
+ ensure
27
+ lock.release
28
+ end
29
+ else
30
+ lock
31
+ end
32
+ end
33
+
34
+ def overloaded?
35
+ limit(cost: 0) { false }
36
+ rescue Overloaded
37
+ true
38
+ end
39
+
40
+ def to_s
41
+ "#<#{self.class}>"
42
+ end
43
+
44
+ def ==(other)
45
+ self.class == other.class &&
46
+ self.key == other.key &&
47
+ self.capacity == other.capacity &&
48
+ self.args == other.args &&
49
+ self.options == other.options &&
50
+ self.redis.connection == other.redis.connection
51
+ end
52
+
53
+ def self.new(*)
54
+ # can only call via subclass
55
+ raise NotImplementedError if self == Berater::Limiter
56
+
57
+ super
58
+ end
59
+
60
+ protected
61
+
62
+ attr_reader :args
63
+
64
+ def initialize(key, capacity, *args, **opts)
65
+ @key = key
66
+ self.capacity = capacity
67
+ @args = args
68
+ @options = opts
69
+ end
70
+
71
+ def capacity=(capacity)
72
+ unless capacity.is_a?(Numeric)
73
+ raise ArgumentError, "expected Numeric, found #{capacity.class}"
74
+ end
75
+
76
+ if capacity == Float::INFINITY
77
+ raise ArgumentError, 'infinite capacity not supported, use Unlimiter'
78
+ end
79
+
80
+ raise ArgumentError, 'capacity must be >= 0' unless capacity >= 0
81
+
82
+ @capacity = capacity
83
+ end
84
+
85
+ def acquire_lock(capacity, cost)
86
+ raise NotImplementedError
87
+ end
88
+
89
+ def cache_key(key)
90
+ "#{self.class}:#{key}"
91
+ end
92
+
93
+ end
94
+ end
data/lib/berater/lock.rb CHANGED
@@ -1,11 +1,10 @@
1
1
  module Berater
2
2
  class Lock
3
3
 
4
- attr_reader :limiter, :id, :contention
4
+ attr_reader :capacity, :contention
5
5
 
6
- def initialize(limiter, id, contention, release_fn = nil)
7
- @limiter = limiter
8
- @id = id
6
+ def initialize(capacity, contention, release_fn = nil)
7
+ @capacity = capacity
9
8
  @contention = contention
10
9
  @locked_at = Time.now
11
10
  @release_fn = release_fn
@@ -13,24 +12,15 @@ module Berater
13
12
  end
14
13
 
15
14
  def locked?
16
- @released_at.nil? && !expired?
17
- end
18
-
19
- def expired?
20
- timeout > 0 && @locked_at + timeout < Time.now
15
+ @released_at.nil?
21
16
  end
22
17
 
23
18
  def release
24
- raise 'lock expired' if expired?
25
19
  raise 'lock already released' unless locked?
26
20
 
27
21
  @released_at = Time.now
28
22
  @release_fn ? @release_fn.call : true
29
23
  end
30
24
 
31
- private def timeout
32
- limiter.respond_to?(:timeout) ? limiter.timeout : 0
33
- end
34
-
35
25
  end
36
26
  end