berater 0.1.4 → 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: c8ca6fba7e38a014eed325df41dda7372b41ff62d9e78b61565f4d915d8b4677
4
- data.tar.gz: 1fae864b93de87ea3dc70531f3f8697e80af526aea53d8ddfe2076eac9bbd61d
3
+ metadata.gz: dec8c5d6d428f795d1489dace166451e2a3a42592d00d71155ed5cd0d5aaa909
4
+ data.tar.gz: 6956b6c2804b6d616074e239439d4f2a8e551e900db7801226948b9eb764d0fc
5
5
  SHA512:
6
- metadata.gz: 1b4b37134edece0657ea9be15119fe25b2208d554b9dbf56edd04f8e5acff77569022be025ec428b81ab1971171ae468e389a9568d7ad0163b0966e622a09f2e
7
- data.tar.gz: bd927303eb1475e79a26161a184205008926f95c59a06eee43f453d5e464d15c205a91bffd36228ae28bc5d27f0fa32faebc7c5870ec5d1779bd6c57ecdc0fe2
6
+ metadata.gz: 327cd2f3a7430c2b81bd1988abd636bfc7d97048c762d3807555e9c1196153570dbe3045fae813852a907ec00b89b8d2c36e9269ed0562419c8d98dd6c1d2b26
7
+ data.tar.gz: ea1cb39e9201091e4a1357e888ac36f630c326a271e3cfcafa13298b8f54424f776f4dd488e046e2178905d0df9fb5c3b1f0d454e331823cafe5dd338fe9ea6c
data/lib/berater.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'berater/version'
2
+ require 'berater/lock'
2
3
 
3
4
 
4
5
  module Berater
@@ -8,41 +9,26 @@ module Berater
8
9
 
9
10
  MODES = {}
10
11
 
11
- attr_accessor :redis, :mode
12
+ attr_accessor :redis
12
13
 
13
14
  def configure
14
- self.mode = :unlimited # default
15
-
16
15
  yield self
17
16
  end
18
17
 
19
- def new(mode, *args, **opts)
18
+ def new(key, mode, *args, **opts)
20
19
  klass = MODES[mode.to_sym]
21
20
 
22
21
  unless klass
23
22
  raise ArgumentError, "invalid mode: #{mode}"
24
23
  end
25
24
 
26
- klass.new(*args, **opts)
25
+ klass.new(key, *args, **opts)
27
26
  end
28
27
 
29
28
  def register(mode, klass)
30
29
  MODES[mode.to_sym] = klass
31
30
  end
32
31
 
33
- def mode=(mode)
34
- unless MODES.include? mode.to_sym
35
- raise ArgumentError, "invalid mode: #{mode}"
36
- end
37
-
38
- @mode = mode.to_sym
39
- end
40
-
41
- def limit(*args, **opts, &block)
42
- mode = opts.delete(:mode) { self.mode }
43
- new(mode, *args, **opts).limit(&block)
44
- end
45
-
46
32
  def expunge
47
33
  redis.scan_each(match: "#{self.name}*") do |key|
48
34
  redis.del key
@@ -51,6 +37,11 @@ module Berater
51
37
 
52
38
  end
53
39
 
40
+ # convenience method
41
+ def Berater(key, mode, *args, **opts, &block)
42
+ Berater.new(key, mode, *args, **opts).limit(&block)
43
+ end
44
+
54
45
  # load and register limiters
55
46
  require 'berater/base_limiter'
56
47
  require 'berater/concurrency_limiter'
@@ -1,31 +1,25 @@
1
1
  module Berater
2
2
  class BaseLimiter
3
3
 
4
- attr_reader :options
5
-
6
- def initialize(**opts)
7
- @options = opts
8
- end
9
-
10
- def key
11
- if options[:key]
12
- "#{self.class}:#{options[:key]}"
13
- else
14
- # default value
15
- self.class.to_s
16
- end
17
- end
4
+ attr_reader :key, :options
18
5
 
19
6
  def redis
20
7
  options[:redis] || Berater.redis
21
8
  end
22
9
 
23
- def limit(**opts)
10
+ def limit
24
11
  raise NotImplementedError
25
12
  end
26
13
 
27
- def self.limit(*args, **opts, &block)
28
- 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}"
29
23
  end
30
24
 
31
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,107 +32,42 @@ module Berater
32
32
  @timeout = timeout
33
33
  end
34
34
 
35
- class Lock
36
- attr_reader :limiter, :id, :contention
37
-
38
- def initialize(limiter, id, contention)
39
- @limiter = limiter
40
- @id = id
41
- @contention = contention
42
- @released = false
43
- @locked_at = Time.now
44
- end
45
-
46
- def release
47
- raise 'lock already released' if released?
48
- raise 'lock expired' if expired?
49
-
50
- @released = limiter.release(self)
51
- end
52
-
53
- def released?
54
- @released
55
- end
56
-
57
- def expired?
58
- limiter.timeout > 0 && @locked_at + limiter.timeout < Time.now
59
- end
60
- end
61
-
62
35
  LUA_SCRIPT = <<~LUA.gsub(/^\s*(--.*\n)?/, '')
63
36
  local key = KEYS[1]
37
+ local lock_key = KEYS[2]
64
38
  local capacity = tonumber(ARGV[1])
65
39
  local ts = tonumber(ARGV[2])
66
40
  local ttl = tonumber(ARGV[3])
67
-
68
- local exists
69
- local count
70
41
  local lock
71
42
 
72
- -- check to see if key already exists
73
- if ttl == 0 then
74
- exists = redis.call('EXISTS', key)
75
- else
76
- -- and refresh TTL while we're at it
77
- exists = redis.call('EXPIRE', key, ttl * 2)
43
+ -- purge stale hosts
44
+ if ttl > 0 then
45
+ redis.call('ZREMRANGEBYSCORE', key, '-inf', ts - ttl)
78
46
  end
79
47
 
80
- if exists == 1 then
81
- -- purge stale hosts
82
- if ttl > 0 then
83
- redis.call('ZREMRANGEBYSCORE', key, '-inf', ts - ttl)
84
- end
85
-
86
- -- check capacity (subtract one for next lock entry)
87
- count = redis.call('ZCARD', key) - 1
88
-
89
- if count < capacity then
90
- -- yay, grab a lock
91
-
92
- -- regenerate next lock entry, which has score inf
93
- lock = unpack(redis.call('ZPOPMAX', key))
94
- redis.call('ZADD', key, 'inf', (lock + 1) % 2^52)
95
-
96
- count = count + 1
97
- end
98
- elseif capacity > 0 then
99
- count = 1
100
- lock = "1"
48
+ -- check capacity
49
+ local count = redis.call('ZCARD', key)
101
50
 
102
- -- create structure to track locks and next id
103
- redis.call('ZADD', key, 'inf', lock + 1)
104
-
105
- if ttl > 0 then
106
- redis.call('EXPIRE', key, ttl * 2)
107
- end
108
- end
109
-
110
- if lock then
111
- -- store lock and timestamp
51
+ if count < capacity then
52
+ -- grab a lock
53
+ lock = redis.call('INCR', lock_key)
112
54
  redis.call('ZADD', key, ts, lock)
55
+ count = count + 1
113
56
  end
114
57
 
115
58
  return { count, lock }
116
59
  LUA
117
60
 
118
- def limit(**opts, &block)
119
- unless opts.empty?
120
- return self.class.new(
121
- capacity,
122
- **options.merge(opts)
123
- # **options.merge(timeout: timeout).merge(opts)
124
- ).limit(&block)
125
- end
126
-
61
+ def limit
127
62
  count, lock_id = redis.eval(
128
63
  LUA_SCRIPT,
129
- [ key ],
64
+ [ cache_key(key), cache_key('lock_id') ],
130
65
  [ capacity, Time.now.to_i, timeout ]
131
66
  )
132
67
 
133
68
  raise Incapacitated unless lock_id
134
69
 
135
- lock = Lock.new(self, lock_id, count)
70
+ lock = Lock.new(self, lock_id, count, -> { release(lock_id) })
136
71
 
137
72
  if block_given?
138
73
  begin
@@ -145,8 +80,8 @@ module Berater
145
80
  end
146
81
  end
147
82
 
148
- def release(lock)
149
- res = redis.zrem(key, lock.id)
83
+ private def release(lock_id)
84
+ res = redis.zrem(cache_key(key), lock_id)
150
85
  res == true || res == 1 # depending on which version of Redis
151
86
  end
152
87
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Berater
2
- VERSION = '0.1.4'
2
+ VERSION = '0.2.0'
3
3
  end
data/spec/berater_spec.rb CHANGED
@@ -9,10 +9,10 @@ describe Berater do
9
9
  describe '.configure' do
10
10
  it 'can be set via configure' do
11
11
  Berater.configure do |c|
12
- c.mode = :rate
12
+ c.redis = :redis
13
13
  end
14
14
 
15
- expect(Berater.mode).to eq :rate
15
+ expect(Berater.redis).to eq :redis
16
16
  end
17
17
  end
18
18
 
@@ -24,20 +24,13 @@ describe Berater do
24
24
  end
25
25
  end
26
26
 
27
- describe '.mode' do
28
- it 'validates inputs' do
29
- expect { Berater.mode = :foo }.to raise_error(ArgumentError)
30
- end
31
- end
32
-
33
- context 'unlimited mode' do
34
- before { Berater.mode = :unlimited }
35
-
36
- describe '.new' do
37
- let(:limiter) { Berater.new(:unlimited) }
27
+ describe '.new' do
28
+ context 'unlimited mode' do
29
+ let(:limiter) { Berater.new(:key, :unlimited) }
38
30
 
39
31
  it 'instantiates an Unlimiter' do
40
32
  expect(limiter).to be_a Berater::Unlimiter
33
+ expect(limiter.key).to be :key
41
34
  end
42
35
 
43
36
  it 'inherits redis' do
@@ -46,35 +39,22 @@ describe Berater do
46
39
 
47
40
  it 'accepts options' do
48
41
  redis = double('Redis')
49
- limiter = Berater.new(:unlimited, key: 'key', redis: redis)
50
- expect(limiter.key).to match(/key/)
42
+ limiter = Berater.new(:key, :unlimited, redis: redis)
51
43
  expect(limiter.redis).to be redis
52
44
  end
53
- end
54
-
55
- describe '.limit' do
56
- it 'works' do
57
- expect(Berater.limit).to be_nil
58
- end
59
-
60
- it 'yields' do
61
- expect {|b| Berater.limit(&b) }.to yield_control
62
- end
63
45
 
64
- it 'never limits' do
65
- 10.times { expect(Berater.limit { 123 } ).to eq 123 }
46
+ it 'works with convinience' do
47
+ expect(Berater).to receive(:new).and_return(limiter)
48
+ expect {|b| Berater(:key, :unlimited, &b) }.to yield_control
66
49
  end
67
50
  end
68
- end
69
-
70
- context 'inhibited mode' do
71
- before { Berater.mode = :inhibited }
72
51
 
73
- describe '.new' do
74
- let(:limiter) { Berater.new(:inhibited) }
52
+ context 'inhibited mode' do
53
+ let(:limiter) { Berater.new(:key, :inhibited) }
75
54
 
76
55
  it 'instantiates an Inhibitor' do
77
56
  expect(limiter).to be_a Berater::Inhibitor
57
+ expect(limiter.key).to be :key
78
58
  end
79
59
 
80
60
  it 'inherits redis' do
@@ -83,27 +63,22 @@ describe Berater do
83
63
 
84
64
  it 'accepts options' do
85
65
  redis = double('Redis')
86
- limiter = Berater.new(:inhibited, key: 'key', redis: redis)
87
- expect(limiter.key).to match(/key/)
66
+ limiter = Berater.new(:key, :inhibited, redis: redis)
88
67
  expect(limiter.redis).to be redis
89
68
  end
90
- end
91
69
 
92
- describe '.limit' do
93
- it 'always limits' do
94
- expect { Berater.limit }.to be_inhibited
70
+ it 'works with convinience' do
71
+ expect(Berater).to receive(:new).and_return(limiter)
72
+ expect { Berater(:key, :inhibited) }.to be_inhibited
95
73
  end
96
74
  end
97
- end
98
-
99
- context 'rate mode' do
100
- before { Berater.mode = :rate }
101
75
 
102
- describe '.limiter' do
103
- let(:limiter) { Berater.new(:rate, 1, :second) }
76
+ context 'rate mode' do
77
+ let(:limiter) { Berater.new(:key, :rate, 1, :second) }
104
78
 
105
79
  it 'instantiates a RateLimiter' do
106
80
  expect(limiter).to be_a Berater::RateLimiter
81
+ expect(limiter.key).to be :key
107
82
  end
108
83
 
109
84
  it 'inherits redis' do
@@ -112,44 +87,22 @@ describe Berater do
112
87
 
113
88
  it 'accepts options' do
114
89
  redis = double('Redis')
115
- limiter = Berater.new(:rate, 1, :second, key: 'key', redis: redis)
116
- expect(limiter.key).to match(/key/)
90
+ limiter = Berater.new(:key, :rate, 1, :second, redis: redis)
117
91
  expect(limiter.redis).to be redis
118
92
  end
119
- end
120
-
121
- describe '.limit' do
122
- it 'works' do
123
- expect(Berater.limit(1, :second)).to eq 1
124
- end
125
-
126
- it 'yields' do
127
- expect {|b| Berater.limit(2, :second, &b) }.to yield_control
128
- expect(Berater.limit(2, :second) { 123 }).to eq 123
129
- end
130
93
 
131
- it 'limits excessive calls' do
132
- expect(Berater.limit(1, :second)).to eq 1
133
- expect { Berater.limit(1, :second) }.to be_overrated
134
- end
135
-
136
- it 'accepts options' do
137
- redis = double('Redis')
138
- expect(redis).to receive(:multi)
139
-
140
- Berater.limit(1, :second, redis: redis) rescue nil
94
+ it 'works with convinience' do
95
+ expect(Berater).to receive(:new).and_return(limiter)
96
+ expect {|b| Berater(:key, :rate, 1, :second, &b) }.to yield_control
141
97
  end
142
98
  end
143
- end
144
99
 
145
- context 'concurrency mode' do
146
- before { Berater.mode = :concurrency }
147
-
148
- describe '.limiter' do
149
- let(:limiter) { Berater.new(:concurrency, 1) }
100
+ context 'concurrency mode' do
101
+ let(:limiter) { Berater.new(:key, :concurrency, 1) }
150
102
 
151
103
  it 'instantiates a ConcurrencyLimiter' do
152
104
  expect(limiter).to be_a Berater::ConcurrencyLimiter
105
+ expect(limiter.key).to be :key
153
106
  end
154
107
 
155
108
  it 'inherits redis' do
@@ -158,33 +111,13 @@ describe Berater do
158
111
 
159
112
  it 'accepts options' do
160
113
  redis = double('Redis')
161
- limiter = Berater.new(:concurrency, 1, key: 'key', redis: redis)
162
- expect(limiter.key).to match(/key/)
114
+ limiter = Berater.new(:key, :concurrency, 1, redis: redis)
163
115
  expect(limiter.redis).to be redis
164
116
  end
165
- end
166
-
167
- describe '.limit' do
168
- it 'works (without blocks by returning a lock)' do
169
- lock = Berater.limit(1)
170
- expect(lock).to be_a Berater::ConcurrencyLimiter::Lock
171
- expect(lock.release).to be true
172
- end
173
-
174
- it 'yields' do
175
- expect {|b| Berater.limit(1, &b) }.to yield_control
176
- end
177
-
178
- it 'limits excessive calls' do
179
- Berater.limit(1)
180
- expect { Berater.limit(1) }.to be_incapacitated
181
- end
182
-
183
- it 'accepts options' do
184
- redis = double('Redis')
185
- expect(redis).to receive(:eval)
186
117
 
187
- Berater.limit(1, redis: redis) rescue nil
118
+ it 'works with convinience' do
119
+ expect(Berater).to receive(:new).and_return(limiter)
120
+ expect {|b| Berater(:key, :concurrency, 1, &b) }.to yield_control
188
121
  end
189
122
  end
190
123
  end
@@ -1,22 +1,20 @@
1
1
  describe Berater::ConcurrencyLimiter do
2
- before { Berater.mode = :concurrency }
3
-
4
2
  describe '.new' do
5
- let(:limiter) { described_class.new(1) }
3
+ let(:limiter) { described_class.new(:key, 1) }
6
4
 
7
5
  it 'initializes' do
6
+ expect(limiter.key).to be :key
8
7
  expect(limiter.capacity).to be 1
9
8
  end
10
9
 
11
10
  it 'has default values' do
12
- expect(limiter.key).to eq described_class.to_s
13
11
  expect(limiter.redis).to be Berater.redis
14
12
  end
15
13
  end
16
14
 
17
15
  describe '#capacity' do
18
16
  def expect_capacity(capacity)
19
- limiter = described_class.new(capacity)
17
+ limiter = described_class.new(:key, capacity)
20
18
  expect(limiter.capacity).to eq capacity
21
19
  end
22
20
 
@@ -27,7 +25,7 @@ describe Berater::ConcurrencyLimiter do
27
25
  context 'with erroneous values' do
28
26
  def expect_bad_capacity(capacity)
29
27
  expect do
30
- described_class.new(capacity)
28
+ described_class.new(:key, capacity)
31
29
  end.to raise_error ArgumentError
32
30
  end
33
31
 
@@ -40,7 +38,7 @@ describe Berater::ConcurrencyLimiter do
40
38
 
41
39
  describe '#timeout' do
42
40
  def expect_timeout(timeout)
43
- limiter = described_class.new(1, timeout: timeout)
41
+ limiter = described_class.new(:key, 1, timeout: timeout)
44
42
  expect(limiter.timeout).to eq timeout
45
43
  end
46
44
 
@@ -51,7 +49,7 @@ describe Berater::ConcurrencyLimiter do
51
49
  context 'with erroneous values' do
52
50
  def expect_bad_timeout(timeout)
53
51
  expect do
54
- described_class.new(1, timeout: timeout)
52
+ described_class.new(:key, 1, timeout: timeout)
55
53
  end.to raise_error ArgumentError
56
54
  end
57
55
 
@@ -63,7 +61,7 @@ describe Berater::ConcurrencyLimiter do
63
61
  end
64
62
 
65
63
  describe '#limit' do
66
- let(:limiter) { described_class.new(2, timeout: 1) }
64
+ let(:limiter) { described_class.new(:key, 2, timeout: 1) }
67
65
 
68
66
  it 'works' do
69
67
  expect {|b| limiter.limit(&b) }.to yield_control
@@ -81,26 +79,26 @@ describe Berater::ConcurrencyLimiter do
81
79
  end
82
80
 
83
81
  it 'limits excessive calls' do
84
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
85
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
82
+ expect(limiter.limit).to be_a Berater::Lock
83
+ expect(limiter.limit).to be_a Berater::Lock
86
84
 
87
85
  expect(limiter).to be_incapacitated
88
86
  end
89
87
 
90
88
  it 'times out locks' do
91
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
92
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
89
+ expect(limiter.limit).to be_a Berater::Lock
90
+ expect(limiter.limit).to be_a Berater::Lock
93
91
  expect(limiter).to be_incapacitated
94
92
 
95
93
  Timecop.travel(1)
96
94
 
97
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
98
- expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
95
+ expect(limiter.limit).to be_a Berater::Lock
96
+ expect(limiter.limit).to be_a Berater::Lock
99
97
  expect(limiter).to be_incapacitated
100
98
  end
101
99
 
102
100
  context 'with capacity 0' do
103
- let(:limiter) { described_class.new(0) }
101
+ let(:limiter) { described_class.new(:key, 0) }
104
102
 
105
103
  it 'always fails' do
106
104
  expect(limiter).to be_incapacitated
@@ -109,60 +107,34 @@ describe Berater::ConcurrencyLimiter do
109
107
  end
110
108
 
111
109
  context 'with same key, different limiters' do
112
- let(:limiter_one) { described_class.new(1) }
113
- let(:limiter_two) { described_class.new(1) }
110
+ let(:limiter_one) { described_class.new(:key, 1) }
111
+ let(:limiter_two) { described_class.new(:key, 1) }
114
112
 
115
113
  it { expect(limiter_one.key).to eq limiter_two.key }
116
114
 
117
115
  it 'works as expected' do
118
- expect(limiter_one.limit).to be_a Berater::ConcurrencyLimiter::Lock
116
+ expect(limiter_one.limit).to be_a Berater::Lock
119
117
 
120
118
  expect(limiter_one).to be_incapacitated
121
119
  expect(limiter_two).to be_incapacitated
122
120
  end
123
121
  end
124
122
 
125
- context 'with different keys, same limiter' do
126
- let(:limiter) { described_class.new(1) }
127
-
128
- it 'works as expected' do
129
- one_lock = limiter.limit(key: :one)
130
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
131
-
132
- expect { limiter.limit(key: :one) {} }.to be_incapacitated
133
- expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
134
-
135
- two_lock = limiter.limit(key: :two)
136
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
137
-
138
- expect { limiter.limit(key: :one) {} }.to be_incapacitated
139
- expect { limiter.limit(key: :two) {} }.to be_incapacitated
140
-
141
- one_lock.release
142
- expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
143
- expect { limiter.limit(key: :two) {} }.to be_incapacitated
144
-
145
- two_lock.release
146
- expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
147
- expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
148
- end
149
- end
150
-
151
123
  context 'with same key, different capacities' do
152
- let(:limiter_one) { described_class.new(1) }
153
- let(:limiter_two) { described_class.new(2) }
124
+ let(:limiter_one) { described_class.new(:key, 1) }
125
+ let(:limiter_two) { described_class.new(:key, 2) }
154
126
 
155
127
  it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
156
128
 
157
129
  it 'works as expected' do
158
130
  one_lock = limiter_one.limit
159
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
131
+ expect(one_lock).to be_a Berater::Lock
160
132
 
161
133
  expect(limiter_one).to be_incapacitated
162
134
  expect(limiter_two).not_to be_incapacitated
163
135
 
164
136
  two_lock = limiter_two.limit
165
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
137
+ expect(two_lock).to be_a Berater::Lock
166
138
 
167
139
  expect(limiter_one).to be_incapacitated
168
140
  expect(limiter_two).to be_incapacitated
@@ -178,21 +150,21 @@ describe Berater::ConcurrencyLimiter do
178
150
  end
179
151
 
180
152
  context 'with different keys, different limiters' do
181
- let(:limiter_one) { described_class.new(1, key: :one) }
182
- let(:limiter_two) { described_class.new(1, key: :two) }
153
+ let(:limiter_one) { described_class.new(:one, 1) }
154
+ let(:limiter_two) { described_class.new(:two, 1) }
183
155
 
184
156
  it 'works as expected' do
185
157
  expect(limiter_one).not_to be_incapacitated
186
158
  expect(limiter_two).not_to be_incapacitated
187
159
 
188
160
  one_lock = limiter_one.limit
189
- expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
161
+ expect(one_lock).to be_a Berater::Lock
190
162
 
191
163
  expect(limiter_one).to be_incapacitated
192
164
  expect(limiter_two).not_to be_incapacitated
193
165
 
194
166
  two_lock = limiter_two.limit
195
- expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
167
+ expect(two_lock).to be_a Berater::Lock
196
168
 
197
169
  expect(limiter_one).to be_incapacitated
198
170
  expect(limiter_two).to be_incapacitated
@@ -1,62 +1,7 @@
1
- describe Berater::ConcurrencyLimiter::Lock do
2
- before { Berater.mode = :concurrency }
1
+ describe Berater::Lock do
2
+ it_behaves_like 'a lock', Berater.new(:key, :concurrency, 3)
3
3
 
4
- let(:limiter) { Berater.new(:concurrency, 3) }
5
-
6
- describe '#contention' do
7
- it 'tracks contention' do
8
- lock_1 = limiter.limit
9
- expect(lock_1.contention).to eq 1
10
-
11
- lock_2 = limiter.limit
12
- expect(lock_2.contention).to eq 2
13
-
14
- limiter.limit do |lock_3|
15
- expect(lock_3.contention).to eq 3
16
- end
17
- end
18
-
19
- it 'works in block mode' do
20
- lock_1 = limiter.limit
21
-
22
- limiter.limit do |lock_2|
23
- expect(lock_1.contention).to eq 1
24
- expect(lock_2.contention).to eq 2
25
- end
26
- end
27
- end
28
-
29
- describe '#release' do
30
- it 'can not be released twice' do
31
- lock = limiter.limit
32
- expect(lock.release).to be true
33
- expect { lock.release }.to raise_error(RuntimeError, /already/)
34
- end
35
-
36
- it 'does not work in block mode' do
37
- expect do
38
- limiter.limit do |lock|
39
- lock.release
40
- end
41
- end.to raise_error(RuntimeError, /already/)
42
- end
43
- end
44
-
45
- describe '#released?' do
46
- it 'works' do
47
- lock = limiter.limit
48
- expect(lock.released?).to be false
49
-
50
- lock.release
51
- expect(lock.released?).to be true
52
- end
53
-
54
- it 'works in block mode' do
55
- limiter.limit do |lock|
56
- expect(lock.released?).to be false
57
- end
58
- end
59
- end
4
+ let(:limiter) { Berater.new(:key, :concurrency, 3) }
60
5
 
61
6
  describe '#expired?' do
62
7
  let!(:lock) { limiter.limit }
@@ -65,10 +10,12 @@ describe Berater::ConcurrencyLimiter::Lock do
65
10
  it { expect(limiter.timeout).to eq 0 }
66
11
 
67
12
  it 'never expires' do
13
+ expect(lock.locked?).to be true
68
14
  expect(lock.expired?).to be false
69
15
 
70
16
  Timecop.travel(1_000)
71
17
 
18
+ expect(lock.locked?).to be true
72
19
  expect(lock.expired?).to be false
73
20
  end
74
21
  end
@@ -76,14 +23,14 @@ describe Berater::ConcurrencyLimiter::Lock do
76
23
  context 'when timeout is set and exceeded' do
77
24
  before { Timecop.travel(1) }
78
25
 
79
- let(:limiter) { Berater.new(:concurrency, 3, timeout: 1) }
26
+ let(:limiter) { Berater.new(:key, :concurrency, 3, timeout: 1) }
80
27
 
81
28
  it 'expires' do
82
29
  expect(lock.expired?).to be true
30
+ expect(lock.locked?).to be false
83
31
  end
84
32
 
85
33
  it 'fails to release' do
86
- expect(lock.released?).to be false
87
34
  expect { lock.release }.to raise_error(RuntimeError, /expired/)
88
35
  end
89
36
  end
@@ -1,36 +1,24 @@
1
1
  describe Berater::Inhibitor do
2
- before { Berater.mode = :inhibited }
3
-
4
2
  describe '.new' do
5
3
  it 'initializes without any arguments or options' do
6
4
  expect(described_class.new).to be_a described_class
7
5
  end
8
6
 
9
7
  it 'initializes with any arguments and options' do
10
- expect(described_class.new(:abc, x: 123)).to be_a described_class
8
+ expect(described_class.new(:abc, :def, x: 123)).to be_a described_class
11
9
  end
12
10
 
13
11
  it 'has default values' do
14
- expect(described_class.new.key).to eq described_class.to_s
12
+ expect(described_class.new.key).to be :inhibitor
15
13
  expect(described_class.new.redis).to be Berater.redis
16
14
  end
17
15
  end
18
16
 
19
- describe '.limit' do
20
- it 'always limits' do
21
- expect { described_class.limit }.to be_inhibited
22
- end
23
-
24
- it 'works with any arguments or options' do
25
- expect { described_class.limit(:abc, x: 123) }.to be_inhibited
26
- end
27
- end
28
-
29
17
  describe '#limit' do
30
18
  let(:limiter) { described_class.new }
31
19
 
32
20
  it 'always limits' do
33
- expect { described_class.limit }.to be_inhibited
21
+ expect { limiter.limit }.to be_inhibited
34
22
  end
35
23
  end
36
24
 
data/spec/matcher_spec.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  describe 'be_overloaded' do
2
2
  context 'Berater::Unlimiter' do
3
- let(:limiter) { Berater.new(:unlimited) }
3
+ let(:limiter) { Berater.new(:key, :unlimited) }
4
4
 
5
5
  it { expect(limiter).not_to be_overloaded }
6
6
  it { expect(limiter).not_to be_inhibited }
@@ -19,7 +19,7 @@ describe 'be_overloaded' do
19
19
  end
20
20
 
21
21
  context 'Berater::Inhibitor' do
22
- let(:limiter) { Berater.new(:inhibited) }
22
+ let(:limiter) { Berater.new(:key, :inhibited) }
23
23
 
24
24
  it { expect(limiter).to be_overloaded }
25
25
  it { expect(limiter).to be_inhibited }
@@ -32,7 +32,7 @@ describe 'be_overloaded' do
32
32
  end
33
33
 
34
34
  context 'Berater::RateLimiter' do
35
- let(:limiter) { Berater.new(:rate, 1, :second) }
35
+ let(:limiter) { Berater.new(:key, :rate, 1, :second) }
36
36
 
37
37
  it { expect(limiter).not_to be_overloaded }
38
38
  it { expect(limiter).not_to be_inhibited }
@@ -67,7 +67,7 @@ describe 'be_overloaded' do
67
67
  end
68
68
 
69
69
  context 'Berater::ConcurrencyLimiter' do
70
- let(:limiter) { Berater.new(:concurrency, 1) }
70
+ let(:limiter) { Berater.new(:key, :concurrency, 1) }
71
71
 
72
72
  it { expect(limiter).not_to be_overloaded }
73
73
  it { expect(limiter).not_to be_inhibited }
@@ -1,23 +1,22 @@
1
1
  describe Berater::RateLimiter do
2
- before { Berater.mode = :rate }
3
2
 
4
3
  describe '.new' do
5
- let(:limiter) { described_class.new(1, :second) }
4
+ let(:limiter) { described_class.new(:key, 1, :second) }
6
5
 
7
6
  it 'initializes' do
7
+ expect(limiter.key).to be :key
8
8
  expect(limiter.count).to eq 1
9
9
  expect(limiter.interval).to eq 1
10
10
  end
11
11
 
12
12
  it 'has default values' do
13
- expect(limiter.key).to eq described_class.to_s
14
13
  expect(limiter.redis).to be Berater.redis
15
14
  end
16
15
  end
17
16
 
18
17
  describe '#count' do
19
18
  def expect_count(count)
20
- limiter = described_class.new(count, :second)
19
+ limiter = described_class.new(:key, count, :second)
21
20
  expect(limiter.count).to eq count
22
21
  end
23
22
 
@@ -28,7 +27,7 @@ describe Berater::RateLimiter do
28
27
  context 'with erroneous values' do
29
28
  def expect_bad_count(count)
30
29
  expect do
31
- described_class.new(count, :second)
30
+ described_class.new(:key, count, :second)
32
31
  end.to raise_error ArgumentError
33
32
  end
34
33
 
@@ -41,7 +40,7 @@ describe Berater::RateLimiter do
41
40
 
42
41
  describe '#interval' do
43
42
  def expect_interval(interval, expected)
44
- limiter = described_class.new(1, interval)
43
+ limiter = described_class.new(:key, 1, interval)
45
44
  expect(limiter.interval).to eq expected
46
45
  end
47
46
 
@@ -73,7 +72,7 @@ describe Berater::RateLimiter do
73
72
  context 'with erroneous values' do
74
73
  def expect_bad_interval(interval)
75
74
  expect do
76
- described_class.new(1, interval)
75
+ described_class.new(:key, 1, interval)
77
76
  end.to raise_error(ArgumentError)
78
77
  end
79
78
 
@@ -84,82 +83,60 @@ describe Berater::RateLimiter do
84
83
  end
85
84
 
86
85
  describe '#limit' do
87
- let(:limiter) { described_class.new(3, :second) }
86
+ let(:limiter) { described_class.new(:key, 3, :second) }
88
87
 
89
88
  it 'works' do
90
- expect(limiter.limit).to eq 1
91
- end
92
-
93
- it 'counts' do
94
- expect(limiter.limit).to eq 1
95
- expect(limiter.limit).to eq 2
96
- expect(limiter.limit).to eq 3
97
- end
98
-
99
- it 'yields' do
100
89
  expect {|b| limiter.limit(&b) }.to yield_control
101
90
  expect(limiter.limit { 123 }).to eq 123
102
91
  end
103
92
 
93
+ it 'works without a block' do
94
+ expect(limiter.limit).to be_a Berater::Lock
95
+ end
96
+
104
97
  it 'limits excessive calls' do
105
98
  3.times { limiter.limit }
106
99
 
107
- expect { limiter.limit }.to be_overrated
100
+ expect(limiter).to be_overrated
108
101
  end
109
102
 
110
103
  it 'limit resets over time' do
111
- expect(limiter.limit).to eq 1
112
- expect(limiter.limit).to eq 2
113
- expect(limiter.limit).to eq 3
104
+ 3.times { limiter.limit }
114
105
  expect(limiter).to be_overrated
115
106
 
116
107
  # travel forward a second
117
108
  Timecop.freeze(1)
118
109
 
119
- expect(limiter.limit).to eq 1
120
- expect(limiter.limit).to eq 2
121
- expect(limiter.limit).to eq 3
110
+ 3.times { limiter.limit }
122
111
  expect(limiter).to be_overrated
123
112
  end
124
113
  end
125
114
 
126
115
  context 'with same key, different limiters' do
127
- let(:limiter_one) { described_class.new(1, :second) }
128
- let(:limiter_two) { described_class.new(1, :second) }
129
-
130
- it 'works as expected' do
131
- expect(limiter_one.limit).to eq 1
132
-
133
- expect { limiter_one }.to be_overrated
134
- expect { limiter_two }.to be_overrated
135
- end
136
- end
137
-
138
- context 'with different keys, same limiter' do
139
- let(:limiter) { described_class.new(1, :second) }
116
+ let(:limiter_one) { described_class.new(:key, 1, :second) }
117
+ let(:limiter_two) { described_class.new(:key, 1, :second) }
140
118
 
141
119
  it 'works as expected' do
142
- expect { limiter.limit(key: :one) }.not_to be_overrated
143
- expect { limiter.limit(key: :one) }.to be_overrated
120
+ expect(limiter_one.limit).not_to be_overrated
144
121
 
145
- expect { limiter.limit(key: :two) }.not_to be_overrated
146
- expect { limiter.limit(key: :two) }.to be_overrated
122
+ expect(limiter_one).to be_overrated
123
+ expect(limiter_two).to be_overrated
147
124
  end
148
125
  end
149
126
 
150
127
  context 'with different keys, different limiters' do
151
- let(:limiter_one) { described_class.new(1, :second, key: :one) }
152
- let(:limiter_two) { described_class.new(2, :second, key: :two) }
128
+ let(:limiter_one) { described_class.new(:one, 1, :second) }
129
+ let(:limiter_two) { described_class.new(:two, 2, :second) }
153
130
 
154
131
  it 'works as expected' do
155
- expect(limiter_one.limit).to eq 1
156
- expect(limiter_two.limit).to eq 1
132
+ expect(limiter_one.limit).not_to be_overrated
133
+ expect(limiter_two.limit).not_to be_overrated
157
134
 
158
- expect { limiter_one.limit }.to be_overrated
159
- expect(limiter_two.limit).to eq 2
135
+ expect(limiter_one).to be_overrated
136
+ expect(limiter_two.limit).not_to be_overrated
160
137
 
161
- expect { limiter_one.limit }.to be_overrated
162
- expect { limiter_two.limit }.to be_overrated
138
+ expect(limiter_one).to be_overrated
139
+ expect(limiter_two).to be_overrated
163
140
  end
164
141
  end
165
142
 
@@ -0,0 +1,20 @@
1
+ describe Berater::Lock do
2
+ it_behaves_like 'a lock', Berater.new(:key, :rate, 3, :second)
3
+
4
+ let(:limiter) { Berater.new(:key, :rate, 3, :second) }
5
+
6
+ describe '#expired?' do
7
+ let!(:lock) { limiter.limit }
8
+
9
+ it 'never expires' do
10
+ expect(lock.locked?).to be true
11
+ expect(lock.expired?).to be false
12
+
13
+ Timecop.travel(1_000)
14
+
15
+ expect(lock.locked?).to be true
16
+ expect(lock.expired?).to be false
17
+ end
18
+ end
19
+
20
+ end
@@ -1,50 +1,28 @@
1
1
  describe Berater::Unlimiter do
2
- before { Berater.mode = :unlimited }
3
-
4
2
  describe '.new' do
5
3
  it 'initializes without any arguments or options' do
6
4
  expect(described_class.new).to be_a described_class
7
5
  end
8
6
 
9
7
  it 'initializes with any arguments and options' do
10
- expect(described_class.new(:abc, x: 123)).to be_a described_class
8
+ expect(described_class.new(:abc, :def, x: 123)).to be_a described_class
11
9
  end
12
10
 
13
11
  it 'has default values' do
14
- expect(described_class.new.key).to eq described_class.to_s
12
+ expect(described_class.new.key).to be :unlimiter
15
13
  expect(described_class.new.redis).to be Berater.redis
16
14
  end
17
15
  end
18
16
 
19
- describe '.limit' do
20
- it 'works' do
21
- expect(described_class.limit).to be_nil
22
- end
23
-
24
- it 'yields' do
25
- expect {|b| described_class.limit(&b) }.to yield_control
26
- end
27
-
28
- it 'is never overloaded' do
29
- 10.times do
30
- expect { described_class.limit }.not_to be_overloaded
31
- end
32
- end
33
-
34
- it 'works with any arguments or options' do
35
- expect(described_class.limit(:abc, x: 123)).to be_nil
36
- end
37
- end
38
-
39
17
  describe '#limit' do
40
18
  let(:limiter) { described_class.new }
41
19
 
42
20
  it 'works' do
43
- expect(limiter.limit).to be_nil
21
+ expect {|b| limiter.limit(&b) }.to yield_control
44
22
  end
45
23
 
46
- it 'yields' do
47
- expect {|b| limiter.limit(&b) }.to yield_control
24
+ it 'works without a block' do
25
+ expect(limiter.limit).to be_a Berater::Lock
48
26
  end
49
27
 
50
28
  it 'is never overloaded' do
@@ -52,10 +30,8 @@ describe Berater::Unlimiter do
52
30
  expect { limiter.limit }.not_to be_overloaded
53
31
  end
54
32
  end
55
-
56
- it 'works with any arguments or options' do
57
- expect(limiter.limit(x: 123)).to be_nil
58
- end
59
33
  end
60
34
 
35
+ it_behaves_like 'a lock', described_class.new
36
+
61
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: berater
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-09 00:00:00.000000000 Z
11
+ date: 2021-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -118,6 +118,7 @@ files:
118
118
  - lib/berater/base_limiter.rb
119
119
  - lib/berater/concurrency_limiter.rb
120
120
  - lib/berater/inhibitor.rb
121
+ - lib/berater/lock.rb
121
122
  - lib/berater/rate_limiter.rb
122
123
  - lib/berater/unlimiter.rb
123
124
  - lib/berater/version.rb
@@ -127,6 +128,7 @@ files:
127
128
  - spec/inhibitor_spec.rb
128
129
  - spec/matcher_spec.rb
129
130
  - spec/rate_limiter_spec.rb
131
+ - spec/rate_lock_spec.rb
130
132
  - spec/unlimiter_spec.rb
131
133
  homepage: https://github.com/dpep/berater_rb
132
134
  licenses:
@@ -154,6 +156,7 @@ summary: Berater
154
156
  test_files:
155
157
  - spec/rate_limiter_spec.rb
156
158
  - spec/matcher_spec.rb
159
+ - spec/rate_lock_spec.rb
157
160
  - spec/concurrency_limiter_spec.rb
158
161
  - spec/concurrency_lock_spec.rb
159
162
  - spec/berater_spec.rb