berater 0.1.4 → 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: 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