berater 0.2.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
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