berater 0.6.2 → 0.7.0

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: dd61012052d49f83a59057ed5ce4cc46b9c25a2d2bf4b5e494f02e590c2e2249
4
- data.tar.gz: 3add1b481830566088eb7a9dce3540716b55dda9bb658a7b26b93a261b403f3a
3
+ metadata.gz: 7d25d186bfb9e709986e5c205c9fd2b8107dcf8df1e915316d28a6a59a0d4c0f
4
+ data.tar.gz: '05380448ad96697b5d3753a93584d4bdfcf3b028c649ca11360a1c92d6afadad'
5
5
  SHA512:
6
- metadata.gz: 476cf552a5fb56498f81a737dba79358d016bedbd76c3c2cc8069c420e82a221c54280781821ef701d25da2551ddc25910818dc400dc7c046cecbe8f6db0ddd2
7
- data.tar.gz: a92e64f027425ecdc4a39ee5f88a792f4f2625ec4bcf65fac70e34d591fb136e7544caa80138474ae5f5e6158fc19ec8894892585cf858966bfe5fe13a3d011b
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, interval = nil, **opts)
22
+ def new(key, capacity, **opts)
23
+ args = []
24
+
23
25
  case capacity
24
- when :unlimited, Float::INFINITY
26
+ when Float::INFINITY
25
27
  Berater::Unlimiter
26
- when :inhibited, 0
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, interval ].compact
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, interval = nil, **opts, &block)
50
- limiter = Berater.new(key, capacity, interval, **opts)
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, '-inf', ts - ttl)
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 check limit, ie. for .overlimit?
39
- if count < capacity then
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 cost is not supported, but make it work
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 Incapacitated if lock_ids.empty?
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, mode = nil, *args, **opts, &block)
5
- if mode.nil?
4
+ def new(key, capacity = nil, **opts, &block)
5
+ if capacity.nil?
6
6
  unless block_given?
7
- raise ArgumentError, 'expected either mode or block'
7
+ raise ArgumentError, 'expected either capacity or block'
8
8
  end
9
9
 
10
- mode, *args = DSL.eval(&block)
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 mode or block, not both'
14
+ raise ArgumentError, 'expected either capacity or block, not both'
14
15
  end
15
16
  end
16
17
 
17
- super(key, mode, *args, **opts)
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
 
@@ -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 Inhibited
9
+ raise Overloaded
14
10
  end
15
11
 
16
12
  end
@@ -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 overloaded?
35
- limit(cost: 0) { false }
36
- rescue Overloaded
37
- true
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
@@ -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
- if last_ts then
37
- count = tonumber(redis.call('GET', key)) or 0
38
-
39
- -- adjust for time passing
40
- local drips = math.floor((ts - last_ts) / msec_per_drip)
41
- count = math.max(0, count - drips)
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 check limit, ie. for .overlimit?
46
- allowed = count < capacity
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 expirations
57
- redis.call('SET', key, count, 'PX', ttl)
58
- redis.call('SET', ts_key, ts, 'PX', ttl)
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), cache_key("#{key}-ts") ],
79
+ [ cache_key(key) ],
73
80
  [ ts, capacity, @interval_msec, cost ]
74
81
  )
75
82
 
76
- raise Overrated unless allowed
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 be_overrated
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
- res.overloaded?
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
- obj.overloaded?
26
+ @limiter.utilization >= 1
31
27
  end
32
- rescue @type
28
+ rescue Berater::Overloaded
33
29
  true
34
30
  end
35
31
 
36
32
  def description
37
33
  if @limiter
38
- "be #{verb}"
34
+ "be overloaded"
39
35
  else
40
- "raise #{@type}"
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 #{verb}"
42
+ "expected to be overloaded"
47
43
  else
48
- "expected #{@type} to be raised"
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 #{verb}"
50
+ "expected not to be overloaded"
55
51
  else
56
- "did not expect #{@type} to be raised"
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(Berater::Overloaded)
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
@@ -19,14 +19,7 @@ module Berater
19
19
  when :pass
20
20
  Lock.new(Float::INFINITY, 0)
21
21
  when :fail
22
- # find class specific Overloaded error
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
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.6.2'
2
+ VERSION = '0.7.0'
3
3
  end
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 'unlimited mode' do
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 'inhibited mode' do
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, *args|
106
+ RSpec.shared_examples 'test convenience' do |klass, capacity, **opts|
107
107
  it 'creates a limiter' do
108
- limiter = Berater(:key, *args)
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, *args)
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, *args) { true }
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