berater 0.1.0 → 0.2.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: 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