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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1a54a65428bf73c9a8840d95900215411a7320d87af5297cbc75039976e182c
4
- data.tar.gz: 5804acf97ae46ab33bf8366438304a301d183fb0b8da48f4b872252769e79a54
3
+ metadata.gz: dec8c5d6d428f795d1489dace166451e2a3a42592d00d71155ed5cd0d5aaa909
4
+ data.tar.gz: 6956b6c2804b6d616074e239439d4f2a8e551e900db7801226948b9eb764d0fc
5
5
  SHA512:
6
- metadata.gz: c4f2f7002db193351b915c0f21b4ff6a704c20e90e7ff5815a5bfef3f38064608a367e7e150cf430e2d87a8b7b10420a3ec00a1c211bc4edcd04d0c479af3eb8
7
- data.tar.gz: '06489eb2f8769e349b163d49b9a6e6a0f1a8d55b97ded73fcdeb01652b0048e4dd3d940fc71075041129b1f280120d44eec906d258056d720c1f07ac23089cb7'
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 = BaseLimiter::Overloaded
8
+ class Overloaded < StandardError; end
13
9
 
14
10
  MODES = {}
15
11
 
16
- attr_accessor :redis, :mode
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)
@@ -1,33 +1,25 @@
1
1
  module Berater
2
2
  class BaseLimiter
3
3
 
4
- class Overloaded < RuntimeError; end
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(**opts)
10
+ def limit
26
11
  raise NotImplementedError
27
12
  end
28
13
 
29
- def self.limit(*args, **opts, &block)
30
- self.new(*args, **opts).limit(&block)
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 ttl = tonumber(ARGV[2])
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
- -- check to see if key already exists
72
- if ttl == 0 then
73
- exists = redis.call('EXISTS', key)
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
- if exists == 1 then
80
- -- purge stale hosts
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
- -- create structure to track locks and next id
102
- redis.call('ZADD', key, 'inf', lock + 1)
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(**opts, &block)
118
- unless opts.empty?
119
- return self.class.new(
120
- capacity,
121
- **options.merge(opts)
122
- # **options.merge(timeout: timeout).merge(opts)
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(lock)
76
+ lock.release
137
77
  end
138
78
  else
139
79
  lock
140
80
  end
141
81
  end
142
82
 
143
- def release(lock)
144
- res = redis.zrem(key, lock.id)
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
@@ -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(**opts, &block)
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
 
@@ -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
@@ -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(**opts, &block)
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
- yield
70
+ begin
71
+ yield lock
72
+ ensure
73
+ lock.release
74
+ end
77
75
  else
78
- count
76
+ lock
79
77
  end
80
78
  end
81
79
 
@@ -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(**opts, &block)
9
- unless opts.empty?
10
- return self.class.new(
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
- yield if block_given?
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