berater 0.6.2 → 0.7.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 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