berater 0.0.1 → 0.1.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
- SHA1:
3
- metadata.gz: 305ca5d94c6d92dd4b8bf73f2da7a03e15d419c7
4
- data.tar.gz: 83a209c930780a32cf5cda81d1c932d1f8756f88
2
+ SHA256:
3
+ metadata.gz: f1a54a65428bf73c9a8840d95900215411a7320d87af5297cbc75039976e182c
4
+ data.tar.gz: 5804acf97ae46ab33bf8366438304a301d183fb0b8da48f4b872252769e79a54
5
5
  SHA512:
6
- metadata.gz: b0970141263af6e4b8461c0b974b01abd0873890d295e613e07db5fbb48ba0b736db44b67638e2eb8aa5f3d220d043ad3e7daaf17e58e00cabe999d67bd66f4d
7
- data.tar.gz: d5d02d7331d13c355f3e3d2c56091552507a30e3ae7d978cbaa555984381c04033bd3d93441e8dd58bee9c019de7dd493a983ad28ad5b3bbd3c3fd23b6af8021
6
+ metadata.gz: c4f2f7002db193351b915c0f21b4ff6a704c20e90e7ff5815a5bfef3f38064608a367e7e150cf430e2d87a8b7b10420a3ec00a1c211bc4edcd04d0c479af3eb8
7
+ data.tar.gz: '06489eb2f8769e349b163d49b9a6e6a0f1a8d55b97ded73fcdeb01652b0048e4dd3d940fc71075041129b1f280120d44eec906d258056d720c1f07ac23089cb7'
data/lib/berater.rb CHANGED
@@ -1,30 +1,62 @@
1
+ require 'berater/base_limiter'
2
+ require 'berater/concurrency_limiter'
3
+ require 'berater/inhibitor'
4
+ require 'berater/rate_limiter'
5
+ require 'berater/unlimiter'
6
+ require 'berater/version'
7
+
8
+
1
9
  module Berater
2
- VERSION = '0.0.1'
10
+ extend self
3
11
 
4
- class << self
12
+ Overloaded = BaseLimiter::Overloaded
5
13
 
6
- def init redis
7
- @@redis = redis
8
- end
14
+ MODES = {}
15
+
16
+ attr_accessor :redis, :mode
9
17
 
18
+ def configure
19
+ self.mode = :unlimited # default
10
20
 
11
- def incr key, limit, seconds
12
- ts = Time.now.to_i
21
+ yield self
22
+ end
13
23
 
14
- # bucket into time slot
15
- rkey = "%s:%s:%d" % [ self.to_s, key, ts - ts % seconds ]
24
+ def new(mode, *args, **opts)
25
+ klass = MODES[mode.to_sym]
16
26
 
17
- count = @@redis.multi do
18
- @@redis.incr rkey
19
- @@redis.expire rkey, seconds * 2
20
- end.first
27
+ unless klass
28
+ raise ArgumentError, "invalid mode: #{mode}"
29
+ end
21
30
 
22
- raise LimitExceeded if count > limit
31
+ klass.new(*args, **opts)
32
+ end
23
33
 
24
- count
34
+ def register(mode, klass)
35
+ MODES[mode.to_sym] = klass
36
+ end
37
+
38
+ def mode=(mode)
39
+ unless MODES.include? mode.to_sym
40
+ raise ArgumentError, "invalid mode: #{mode}"
25
41
  end
26
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
+ def expunge
52
+ redis.scan_each(match: "#{self.name}*") do |key|
53
+ redis.del key
54
+ end
27
55
  end
28
56
 
29
- class LimitExceeded < RuntimeError; end
30
57
  end
58
+
59
+ Berater.register(:concurrency, Berater::ConcurrencyLimiter)
60
+ Berater.register(:inhibited, Berater::Inhibitor)
61
+ Berater.register(:rate, Berater::RateLimiter)
62
+ Berater.register(:unlimited, Berater::Unlimiter)
@@ -0,0 +1,34 @@
1
+ module Berater
2
+ class BaseLimiter
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
20
+
21
+ def redis
22
+ options[:redis] || Berater.redis
23
+ end
24
+
25
+ def limit(**opts)
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def self.limit(*args, **opts, &block)
30
+ self.new(*args, **opts).limit(&block)
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,149 @@
1
+ module Berater
2
+ class ConcurrencyLimiter < BaseLimiter
3
+
4
+ class Incapacitated < Overloaded; end
5
+
6
+ attr_reader :capacity, :timeout
7
+
8
+ def initialize(capacity, **opts)
9
+ super(**opts)
10
+
11
+ self.capacity = capacity
12
+ self.timeout = opts[:timeout] || 0
13
+ end
14
+
15
+ def capacity=(capacity)
16
+ unless capacity.is_a? Integer
17
+ raise ArgumentError, "expected Integer, found #{capacity.class}"
18
+ end
19
+
20
+ raise ArgumentError, "capacity must be >= 0" unless capacity >= 0
21
+
22
+ @capacity = capacity
23
+ end
24
+
25
+ def timeout=(timeout)
26
+ unless timeout.is_a? Integer
27
+ raise ArgumentError, "expected Integer, found #{timeout.class}"
28
+ end
29
+
30
+ raise ArgumentError, "timeout must be >= 0" unless timeout >= 0
31
+
32
+ @timeout = timeout
33
+ end
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
+ LUA_SCRIPT = <<~LUA.gsub(/^\s*(--.*\n)?/, '')
62
+ local key = KEYS[1]
63
+ local capacity = tonumber(ARGV[1])
64
+ local ttl = tonumber(ARGV[2])
65
+
66
+ local exists
67
+ local count
68
+ local lock
69
+ local ts = unpack(redis.call('TIME'))
70
+
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)
77
+ end
78
+
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"
100
+
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
111
+ redis.call('ZADD', key, ts, lock)
112
+ end
113
+
114
+ return { count, lock }
115
+ LUA
116
+
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 ])
127
+
128
+ raise Incapacitated unless lock_id
129
+
130
+ lock = Lock.new(self, lock_id)
131
+
132
+ if block_given?
133
+ begin
134
+ yield
135
+ ensure
136
+ release(lock)
137
+ end
138
+ else
139
+ lock
140
+ end
141
+ end
142
+
143
+ def release(lock)
144
+ res = redis.zrem(key, lock.id)
145
+ res == true || res == 1
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,21 @@
1
+ module Berater
2
+ class Inhibitor < BaseLimiter
3
+
4
+ class Inhibited < Overloaded; end
5
+
6
+ def initialize(*args, **opts)
7
+ super(**opts)
8
+ end
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
+
17
+ raise Inhibited
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,84 @@
1
+ module Berater
2
+ class RateLimiter < BaseLimiter
3
+
4
+ class Overrated < Overloaded; end
5
+
6
+ attr_accessor :count, :interval
7
+
8
+ def initialize(count, interval, **opts)
9
+ super(**opts)
10
+
11
+ self.count = count
12
+ self.interval = interval
13
+ end
14
+
15
+ def count=(count)
16
+ unless count.is_a? Integer
17
+ raise ArgumentError, "expected Integer, found #{count.class}"
18
+ end
19
+
20
+ raise ArgumentError, "count must be >= 0" unless count >= 0
21
+
22
+ @count = count
23
+ end
24
+
25
+ def interval=(interval)
26
+ @interval = interval.dup
27
+
28
+ case @interval
29
+ when Integer
30
+ raise ArgumentError, "interval must be >= 0" unless @interval >= 0
31
+ when String
32
+ @interval = @interval.to_sym
33
+ when Symbol
34
+ else
35
+ raise ArgumentError, "unexpected interval type: #{interval.class}"
36
+ end
37
+
38
+ if @interval.is_a? Symbol
39
+ case @interval
40
+ when :sec, :second, :seconds
41
+ @interval = 1
42
+ when :min, :minute, :minutes
43
+ @interval = 60
44
+ when :hour, :hours
45
+ @interval = 60 * 60
46
+ else
47
+ raise ArgumentError, "unexpected interval value: #{interval}"
48
+ end
49
+ end
50
+
51
+ @interval
52
+ end
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
+
63
+ ts = Time.now.to_i
64
+
65
+ # bucket into time slot
66
+ rkey = "%s:%d" % [ key, ts - ts % @interval ]
67
+
68
+ count, _ = redis.multi do
69
+ redis.incr rkey
70
+ redis.expire rkey, @interval * 2
71
+ end
72
+
73
+ raise Overrated if count > @count
74
+
75
+ if block_given?
76
+ yield
77
+ else
78
+ count
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+
@@ -0,0 +1,19 @@
1
+ module Berater
2
+ class Unlimiter < BaseLimiter
3
+
4
+ def initialize(*args, **opts)
5
+ super(**opts)
6
+ end
7
+
8
+ def limit(**opts, &block)
9
+ unless opts.empty?
10
+ return self.class.new(
11
+ **options.merge(opts)
12
+ ).limit(&block)
13
+ end
14
+
15
+ yield if block_given?
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module Berater
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,192 @@
1
+ describe Berater do
2
+
3
+ it 'is connected to Redis' do
4
+ expect(Berater.redis.ping).to eq 'PONG'
5
+ end
6
+
7
+ it { is_expected.to respond_to :configure }
8
+
9
+ describe '.configure' do
10
+ it 'can be set via configure' do
11
+ Berater.configure do |c|
12
+ c.mode = :rate
13
+ end
14
+
15
+ expect(Berater.mode).to eq :rate
16
+ end
17
+ end
18
+
19
+ describe '.redis' do
20
+ it 'can be reset' do
21
+ expect(Berater.redis).not_to be_nil
22
+ Berater.redis = nil
23
+ expect(Berater.redis).to be_nil
24
+ end
25
+ end
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) }
38
+
39
+ it 'instantiates an Unlimiter' do
40
+ expect(limiter).to be_a Berater::Unlimiter
41
+ end
42
+
43
+ it 'inherits redis' do
44
+ expect(limiter.redis).to be Berater.redis
45
+ end
46
+
47
+ it 'accepts options' do
48
+ redis = double('Redis')
49
+ limiter = Berater.new(:unlimited, key: 'key', redis: redis)
50
+ expect(limiter.key).to match(/key/)
51
+ expect(limiter.redis).to be redis
52
+ 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
+
64
+ it 'never limits' do
65
+ 10.times { expect(Berater.limit { 123 } ).to eq 123 }
66
+ end
67
+ end
68
+ end
69
+
70
+ context 'inhibited mode' do
71
+ before { Berater.mode = :inhibited }
72
+
73
+ describe '.new' do
74
+ let(:limiter) { Berater.new(:inhibited) }
75
+
76
+ it 'instantiates an Inhibitor' do
77
+ expect(limiter).to be_a Berater::Inhibitor
78
+ end
79
+
80
+ it 'inherits redis' do
81
+ expect(limiter.redis).to be Berater.redis
82
+ end
83
+
84
+ it 'accepts options' do
85
+ redis = double('Redis')
86
+ limiter = Berater.new(:inhibited, key: 'key', redis: redis)
87
+ expect(limiter.key).to match(/key/)
88
+ expect(limiter.redis).to be redis
89
+ end
90
+ end
91
+
92
+ describe '.limit' do
93
+ it 'always limits' do
94
+ expect { Berater.limit }.to be_inhibited
95
+ end
96
+ end
97
+ end
98
+
99
+ context 'rate mode' do
100
+ before { Berater.mode = :rate }
101
+
102
+ describe '.limiter' do
103
+ let(:limiter) { Berater.new(:rate, 1, :second) }
104
+
105
+ it 'instantiates a RateLimiter' do
106
+ expect(limiter).to be_a Berater::RateLimiter
107
+ end
108
+
109
+ it 'inherits redis' do
110
+ expect(limiter.redis).to be Berater.redis
111
+ end
112
+
113
+ it 'accepts options' do
114
+ redis = double('Redis')
115
+ limiter = Berater.new(:rate, 1, :second, key: 'key', redis: redis)
116
+ expect(limiter.key).to match(/key/)
117
+ expect(limiter.redis).to be redis
118
+ 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
+
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
141
+ end
142
+ end
143
+ end
144
+
145
+ context 'concurrency mode' do
146
+ before { Berater.mode = :concurrency }
147
+
148
+ describe '.limiter' do
149
+ let(:limiter) { Berater.new(:concurrency, 1) }
150
+
151
+ it 'instantiates a ConcurrencyLimiter' do
152
+ expect(limiter).to be_a Berater::ConcurrencyLimiter
153
+ end
154
+
155
+ it 'inherits redis' do
156
+ expect(limiter.redis).to be Berater.redis
157
+ end
158
+
159
+ it 'accepts options' do
160
+ redis = double('Redis')
161
+ limiter = Berater.new(:concurrency, 1, key: 'key', redis: redis)
162
+ expect(limiter.key).to match(/key/)
163
+ expect(limiter.redis).to be redis
164
+ 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
+
187
+ Berater.limit(1, redis: redis) rescue nil
188
+ end
189
+ end
190
+ end
191
+
192
+ end
@@ -0,0 +1,189 @@
1
+ describe Berater::ConcurrencyLimiter do
2
+ before { Berater.mode = :concurrency }
3
+
4
+ describe '.new' do
5
+ let(:limiter) { described_class.new(1) }
6
+
7
+ it 'initializes' do
8
+ expect(limiter.capacity).to be 1
9
+ end
10
+
11
+ it 'has default values' do
12
+ expect(limiter.key).to eq described_class.to_s
13
+ expect(limiter.redis).to be Berater.redis
14
+ end
15
+ end
16
+
17
+ describe '#capacity' do
18
+ def expect_capacity(capacity)
19
+ limiter = described_class.new(capacity)
20
+ expect(limiter.capacity).to eq capacity
21
+ end
22
+
23
+ it { expect_capacity(0) }
24
+ it { expect_capacity(1) }
25
+ it { expect_capacity(10_000) }
26
+
27
+ context 'with erroneous values' do
28
+ def expect_bad_capacity(capacity)
29
+ expect do
30
+ described_class.new(capacity)
31
+ end.to raise_error ArgumentError
32
+ end
33
+
34
+ it { expect_bad_capacity(0.5) }
35
+ it { expect_bad_capacity(-1) }
36
+ it { expect_bad_capacity('1') }
37
+ it { expect_bad_capacity(:one) }
38
+ end
39
+ end
40
+
41
+ describe '#timeout' do
42
+ def expect_timeout(timeout)
43
+ limiter = described_class.new(1, timeout: timeout)
44
+ expect(limiter.timeout).to eq timeout
45
+ end
46
+
47
+ it { expect_timeout(0) }
48
+ it { expect_timeout(1) }
49
+ it { expect_timeout(10_000) }
50
+
51
+ context 'with erroneous values' do
52
+ def expect_bad_timeout(timeout)
53
+ expect do
54
+ described_class.new(1, timeout: timeout)
55
+ end.to raise_error ArgumentError
56
+ end
57
+
58
+ it { expect_bad_timeout(0.5) }
59
+ it { expect_bad_timeout(-1) }
60
+ it { expect_bad_timeout('1') }
61
+ it { expect_bad_timeout(:one) }
62
+ end
63
+ end
64
+
65
+ describe '#limit' do
66
+ let(:limiter) { described_class.new(2, timeout: 1) }
67
+
68
+ it 'works' do
69
+ expect {|b| limiter.limit(&b) }.to yield_control
70
+ end
71
+
72
+ it 'works many times if workers complete and return locks' do
73
+ 30.times do
74
+ expect {|b| limiter.limit(&b) }.to yield_control
75
+ end
76
+ end
77
+
78
+ it 'limits excessive calls' do
79
+ expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
80
+ expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
81
+
82
+ expect { limiter }.to be_incapacitated
83
+ end
84
+
85
+ it 'times out locks' do
86
+ expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
87
+ expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
88
+ expect { limiter }.to be_incapacitated
89
+
90
+ sleep(1)
91
+
92
+ expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
93
+ expect(limiter.limit).to be_a Berater::ConcurrencyLimiter::Lock
94
+ expect { limiter }.to be_incapacitated
95
+ end
96
+ end
97
+
98
+ context 'with same key, different limiters' do
99
+ let(:limiter_one) { described_class.new(1) }
100
+ let(:limiter_two) { described_class.new(1) }
101
+
102
+ it { expect(limiter_one.key).to eq limiter_two.key }
103
+
104
+ it 'works as expected' do
105
+ expect(limiter_one.limit).to be_a Berater::ConcurrencyLimiter::Lock
106
+
107
+ expect { limiter_one }.to be_incapacitated
108
+ expect { limiter_two }.to be_incapacitated
109
+ end
110
+ end
111
+
112
+ context 'with different keys, same limiter' do
113
+ let(:limiter) { described_class.new(1) }
114
+
115
+ it 'works as expected' do
116
+ one_lock = limiter.limit(key: :one)
117
+ expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
118
+
119
+ expect { limiter.limit(key: :one) {} }.to be_incapacitated
120
+ expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
121
+
122
+ two_lock = limiter.limit(key: :two)
123
+ expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
124
+
125
+ expect { limiter.limit(key: :one) {} }.to be_incapacitated
126
+ expect { limiter.limit(key: :two) {} }.to be_incapacitated
127
+
128
+ one_lock.release
129
+ expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
130
+ expect { limiter.limit(key: :two) {} }.to be_incapacitated
131
+
132
+ two_lock.release
133
+ expect { limiter.limit(key: :one) {} }.not_to be_incapacitated
134
+ expect { limiter.limit(key: :two) {} }.not_to be_incapacitated
135
+ end
136
+ end
137
+
138
+ context 'with same key, different capacities' do
139
+ let(:limiter_one) { described_class.new(1) }
140
+ let(:limiter_two) { described_class.new(2) }
141
+
142
+ it { expect(limiter_one.capacity).not_to eq limiter_two.capacity }
143
+
144
+ it 'works as expected' do
145
+ one_lock = limiter_one.limit
146
+ expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
147
+
148
+ expect { limiter_one }.to be_incapacitated
149
+ expect { limiter_two }.not_to be_incapacitated
150
+
151
+ two_lock = limiter_two.limit
152
+ expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
153
+
154
+ expect { limiter_one }.to be_incapacitated
155
+ expect { limiter_two }.to be_incapacitated
156
+
157
+ one_lock.release
158
+ expect { limiter_one }.to be_incapacitated
159
+ expect { limiter_two }.not_to be_incapacitated
160
+
161
+ two_lock.release
162
+ expect { limiter_one }.not_to be_incapacitated
163
+ expect { limiter_two }.not_to be_incapacitated
164
+ end
165
+ end
166
+
167
+ context 'with different keys, different limiters' do
168
+ let(:limiter_one) { described_class.new(1, key: :one) }
169
+ let(:limiter_two) { described_class.new(1, key: :two) }
170
+
171
+ it 'works as expected' do
172
+ expect { limiter_one }.not_to be_incapacitated
173
+ expect { limiter_two }.not_to be_incapacitated
174
+
175
+ one_lock = limiter_one.limit
176
+ expect(one_lock).to be_a Berater::ConcurrencyLimiter::Lock
177
+
178
+ expect { limiter_one }.to be_incapacitated
179
+ expect { limiter_two }.not_to be_incapacitated
180
+
181
+ two_lock = limiter_two.limit
182
+ expect(two_lock).to be_a Berater::ConcurrencyLimiter::Lock
183
+
184
+ expect { limiter_one }.to be_incapacitated
185
+ expect { limiter_two }.to be_incapacitated
186
+ end
187
+ end
188
+
189
+ end
@@ -0,0 +1,34 @@
1
+ describe Berater::ConcurrencyLimiter::Lock do
2
+ subject { Berater.limit(1, timeout: 1) }
3
+
4
+ before { Berater.mode = :concurrency }
5
+
6
+ it { expect(subject.released?).to be false }
7
+ it { expect(subject.expired?).to be false }
8
+
9
+ context 'after being released' do
10
+ before { subject.release }
11
+
12
+ it { expect(subject.released?).to be true }
13
+ it { expect(subject.expired?).to be false }
14
+
15
+ it 'can not be released again' do
16
+ expect { subject.release }.to raise_error(RuntimeError, /already/)
17
+ end
18
+ end
19
+
20
+ context 'when enough time passes' do
21
+ before { subject; Timecop.freeze(2) }
22
+
23
+ it 'expires' do
24
+ expect(subject.expired?).to be true
25
+ end
26
+
27
+ it 'fails to release' do
28
+ expect { subject.release }.to raise_error(RuntimeError, /expired/)
29
+ end
30
+
31
+ it { expect(subject.released?).to be false }
32
+ end
33
+
34
+ end
@@ -0,0 +1,37 @@
1
+ describe Berater::Inhibitor do
2
+ before { Berater.mode = :inhibited }
3
+
4
+ describe '.new' do
5
+ it 'initializes without any arguments or options' do
6
+ expect(described_class.new).to be_a described_class
7
+ end
8
+
9
+ it 'initializes with any arguments and options' do
10
+ expect(described_class.new(:abc, x: 123)).to be_a described_class
11
+ end
12
+
13
+ it 'has default values' do
14
+ expect(described_class.new.key).to eq described_class.to_s
15
+ expect(described_class.new.redis).to be Berater.redis
16
+ end
17
+ end
18
+
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
+ describe '#limit' do
30
+ let(:limiter) { described_class.new }
31
+
32
+ it 'always limits' do
33
+ expect { described_class.limit }.to be_inhibited
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,118 @@
1
+ describe 'be_overloaded' do
2
+ context 'Berater::Unlimiter' do
3
+ let(:limiter) { Berater.new(:unlimited) }
4
+
5
+ it { expect(limiter).not_to be_overloaded }
6
+ it { expect(limiter).not_to be_inhibited }
7
+ it { expect(limiter).not_to be_overrated }
8
+ it { expect(limiter).not_to be_incapacitated }
9
+
10
+ it { expect { limiter }.not_to be_overloaded }
11
+ it { expect { limiter }.not_to be_inhibited }
12
+ it { expect { limiter }.not_to be_overrated }
13
+ it { expect { limiter }.not_to be_incapacitated }
14
+
15
+ it { expect { limiter.limit }.not_to be_overloaded }
16
+ it { expect { limiter.limit }.not_to be_inhibited }
17
+ it { expect { limiter.limit }.not_to be_overrated }
18
+ it { expect { limiter.limit }.not_to be_incapacitated }
19
+ end
20
+
21
+ context 'Berater::Inhibitor' do
22
+ let(:limiter) { Berater.new(:inhibited) }
23
+
24
+ it { expect(limiter).to be_overloaded }
25
+ it { expect(limiter).to be_inhibited }
26
+
27
+ it { expect { limiter }.to be_overloaded }
28
+ it { expect { limiter }.to be_inhibited }
29
+
30
+ it { expect { limiter.limit }.to be_overloaded }
31
+ it { expect { limiter.limit }.to be_inhibited }
32
+ end
33
+
34
+ context 'Berater::RateLimiter' do
35
+ let(:limiter) { Berater.new(:rate, 1, :second) }
36
+
37
+ it { expect(limiter).not_to be_overloaded }
38
+ it { expect(limiter).not_to be_inhibited }
39
+ it { expect(limiter).not_to be_overrated }
40
+ it { expect(limiter).not_to be_incapacitated }
41
+
42
+ it { expect { limiter }.not_to be_overloaded }
43
+ it { expect { limiter }.not_to be_inhibited }
44
+ it { expect { limiter }.not_to be_overrated }
45
+ it { expect { limiter }.not_to be_incapacitated }
46
+
47
+ it { expect { limiter.limit }.not_to be_overloaded }
48
+ it { expect { limiter.limit }.not_to be_inhibited }
49
+ it { expect { limiter.limit }.not_to be_overrated }
50
+ it { expect { limiter.limit }.not_to be_incapacitated }
51
+
52
+ context 'once limit is used up' do
53
+ before { limiter.limit }
54
+
55
+ it 'should be_overrated' do
56
+ expect(limiter).to be_overrated
57
+ end
58
+
59
+ it 'should be_overrated' do
60
+ expect { limiter }.to be_overrated
61
+ end
62
+
63
+ it 'should be_overrated' do
64
+ expect { limiter.limit }.to be_overrated
65
+ end
66
+ end
67
+ end
68
+
69
+ context 'Berater::ConcurrencyLimiter' do
70
+ let(:limiter) { Berater.new(:concurrency, 1) }
71
+
72
+ it { expect(limiter).not_to be_overloaded }
73
+ it { expect(limiter).not_to be_inhibited }
74
+ it { expect(limiter).not_to be_overrated }
75
+ it { expect(limiter).not_to be_incapacitated }
76
+
77
+ it { expect { limiter }.not_to be_overloaded }
78
+ it { expect { limiter }.not_to be_inhibited }
79
+ it { expect { limiter }.not_to be_overrated }
80
+ it { expect { limiter }.not_to be_incapacitated }
81
+
82
+ it { expect { limiter.limit }.not_to be_overloaded }
83
+ it { expect { limiter.limit }.not_to be_inhibited }
84
+ it { expect { limiter.limit }.not_to be_overrated }
85
+ it { expect { limiter.limit }.not_to be_incapacitated }
86
+
87
+ context 'when lock is released' do
88
+ it 'should be_incapacitated' do
89
+ 3.times do
90
+ expect(limiter).not_to be_incapacitated
91
+ end
92
+ end
93
+
94
+ it 'should be_incapacitated' do
95
+ 3.times do
96
+ expect { limiter }.not_to be_incapacitated
97
+ end
98
+ end
99
+
100
+ it 'should be_incapacitated' do
101
+ 3.times do
102
+ expect { limiter.limit {} }.not_to be_incapacitated
103
+ end
104
+ end
105
+ end
106
+
107
+ context 'when lock is *not* released' do
108
+ it 'should be_incapacitated' do
109
+ expect { limiter.limit }.not_to be_incapacitated
110
+ expect { limiter.limit }.to be_incapacitated
111
+ end
112
+
113
+ it 'should be_incapacitated' do
114
+ expect { 3.times { limiter.limit } }.to be_incapacitated
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,166 @@
1
+ describe Berater::RateLimiter do
2
+ before { Berater.mode = :rate }
3
+
4
+ describe '.new' do
5
+ let(:limiter) { described_class.new(1, :second) }
6
+
7
+ it 'initializes' do
8
+ expect(limiter.count).to eq 1
9
+ expect(limiter.interval).to eq 1
10
+ end
11
+
12
+ it 'has default values' do
13
+ expect(limiter.key).to eq described_class.to_s
14
+ expect(limiter.redis).to be Berater.redis
15
+ end
16
+ end
17
+
18
+ describe '#count' do
19
+ def expect_count(count)
20
+ limiter = described_class.new(count, :second)
21
+ expect(limiter.count).to eq count
22
+ end
23
+
24
+ it { expect_count(0) }
25
+ it { expect_count(1) }
26
+ it { expect_count(100) }
27
+
28
+ context 'with erroneous values' do
29
+ def expect_bad_count(count)
30
+ expect do
31
+ described_class.new(count, :second)
32
+ end.to raise_error ArgumentError
33
+ end
34
+
35
+ it { expect_bad_count(0.5) }
36
+ it { expect_bad_count(-1) }
37
+ it { expect_bad_count('1') }
38
+ it { expect_bad_count(:one) }
39
+ end
40
+ end
41
+
42
+ describe '#interval' do
43
+ def expect_interval(interval, expected)
44
+ limiter = described_class.new(1, interval)
45
+ expect(limiter.interval).to eq expected
46
+ end
47
+
48
+ context 'with ints' do
49
+ it { expect_interval(0, 0) }
50
+ it { expect_interval(1, 1) }
51
+ it { expect_interval(33, 33) }
52
+ end
53
+
54
+ context 'with symbols' do
55
+ it { expect_interval(:sec, 1) }
56
+ it { expect_interval(:second, 1) }
57
+ it { expect_interval(:seconds, 1) }
58
+
59
+ it { expect_interval(:min, 60) }
60
+ it { expect_interval(:minute, 60) }
61
+ it { expect_interval(:minutes, 60) }
62
+
63
+ it { expect_interval(:hour, 3600) }
64
+ it { expect_interval(:hours, 3600) }
65
+ end
66
+
67
+ context 'with strings' do
68
+ it { expect_interval('sec', 1) }
69
+ it { expect_interval('minute', 60) }
70
+ it { expect_interval('hours', 3600) }
71
+ end
72
+
73
+ context 'with erroneous values' do
74
+ def expect_bad_interval(interval)
75
+ expect do
76
+ described_class.new(1, interval)
77
+ end.to raise_error(ArgumentError)
78
+ end
79
+
80
+ it { expect_bad_interval(-1) }
81
+ it { expect_bad_interval(:secondz) }
82
+ it { expect_bad_interval('huor') }
83
+ end
84
+ end
85
+
86
+ describe '#limit' do
87
+ let(:limiter) { described_class.new(3, :second) }
88
+
89
+ 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
+ expect {|b| limiter.limit(&b) }.to yield_control
101
+ expect(limiter.limit { 123 }).to eq 123
102
+ end
103
+
104
+ it 'limits excessive calls' do
105
+ 3.times { limiter.limit }
106
+
107
+ expect { limiter.limit }.to be_overrated
108
+ end
109
+
110
+ 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
114
+ expect(limiter).to be_overrated
115
+
116
+ # travel forward a second
117
+ Timecop.freeze(1)
118
+
119
+ expect(limiter.limit).to eq 1
120
+ expect(limiter.limit).to eq 2
121
+ expect(limiter.limit).to eq 3
122
+ expect(limiter).to be_overrated
123
+ end
124
+ end
125
+
126
+ 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) }
140
+
141
+ it 'works as expected' do
142
+ expect { limiter.limit(key: :one) }.not_to be_overrated
143
+ expect { limiter.limit(key: :one) }.to be_overrated
144
+
145
+ expect { limiter.limit(key: :two) }.not_to be_overrated
146
+ expect { limiter.limit(key: :two) }.to be_overrated
147
+ end
148
+ end
149
+
150
+ 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) }
153
+
154
+ it 'works as expected' do
155
+ expect(limiter_one.limit).to eq 1
156
+ expect(limiter_two.limit).to eq 1
157
+
158
+ expect { limiter_one.limit }.to be_overrated
159
+ expect(limiter_two.limit).to eq 2
160
+
161
+ expect { limiter_one.limit }.to be_overrated
162
+ expect { limiter_two.limit }.to be_overrated
163
+ end
164
+ end
165
+
166
+ end
@@ -0,0 +1,61 @@
1
+ describe Berater::Unlimiter do
2
+ before { Berater.mode = :unlimited }
3
+
4
+ describe '.new' do
5
+ it 'initializes without any arguments or options' do
6
+ expect(described_class.new).to be_a described_class
7
+ end
8
+
9
+ it 'initializes with any arguments and options' do
10
+ expect(described_class.new(:abc, x: 123)).to be_a described_class
11
+ end
12
+
13
+ it 'has default values' do
14
+ expect(described_class.new.key).to eq described_class.to_s
15
+ expect(described_class.new.redis).to be Berater.redis
16
+ end
17
+ end
18
+
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
+ describe '#limit' do
40
+ let(:limiter) { described_class.new }
41
+
42
+ it 'works' do
43
+ expect(limiter.limit).to be_nil
44
+ end
45
+
46
+ it 'yields' do
47
+ expect {|b| limiter.limit(&b) }.to yield_control
48
+ end
49
+
50
+ it 'is never overloaded' do
51
+ 10.times do
52
+ expect { limiter.limit }.not_to be_overloaded
53
+ end
54
+ end
55
+
56
+ it 'works with any arguments or options' do
57
+ expect(limiter.limit(x: 123)).to be_nil
58
+ end
59
+ end
60
+
61
+ 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.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-29 00:00:00.000000000 Z
11
+ date: 2021-02-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -25,7 +25,21 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: clockwork
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: codecov
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
@@ -42,42 +56,83 @@ dependencies:
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
- - - "~>"
59
+ - - ">="
46
60
  - !ruby/object:Gem::Version
47
- version: '10'
61
+ version: '0'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
- - - "~>"
66
+ - - ">="
53
67
  - !ruby/object:Gem::Version
54
- version: '10'
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
- name: minitest
70
+ name: rspec
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
- - - "~>"
73
+ - - ">="
60
74
  - !ruby/object:Gem::Version
61
- version: '5'
75
+ version: '0'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
- - - "~>"
80
+ - - ">="
67
81
  - !ruby/object:Gem::Version
68
- version: '5'
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: timecop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
69
111
  description: rate limiter
70
- email:
112
+ email:
71
113
  executables: []
72
114
  extensions: []
73
115
  extra_rdoc_files: []
74
116
  files:
75
117
  - lib/berater.rb
76
- homepage: https://github.com/dpep/berater
118
+ - lib/berater/base_limiter.rb
119
+ - lib/berater/concurrency_limiter.rb
120
+ - lib/berater/inhibitor.rb
121
+ - lib/berater/rate_limiter.rb
122
+ - lib/berater/unlimiter.rb
123
+ - lib/berater/version.rb
124
+ - spec/berater_spec.rb
125
+ - spec/concurrency_limiter_spec.rb
126
+ - spec/concurrency_lock_spec.rb
127
+ - spec/inhibitor_spec.rb
128
+ - spec/matcher_spec.rb
129
+ - spec/rate_limiter_spec.rb
130
+ - spec/unlimiter_spec.rb
131
+ homepage: https://github.com/dpep/berater_rb
77
132
  licenses:
78
133
  - MIT
79
134
  metadata: {}
80
- post_install_message:
135
+ post_install_message:
81
136
  rdoc_options: []
82
137
  require_paths:
83
138
  - lib
@@ -92,9 +147,15 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
147
  - !ruby/object:Gem::Version
93
148
  version: '0'
94
149
  requirements: []
95
- rubyforge_project:
96
- rubygems_version: 2.6.11
97
- signing_key:
150
+ rubygems_version: 3.0.8
151
+ signing_key:
98
152
  specification_version: 4
99
153
  summary: Berater
100
- test_files: []
154
+ test_files:
155
+ - spec/rate_limiter_spec.rb
156
+ - spec/matcher_spec.rb
157
+ - spec/concurrency_limiter_spec.rb
158
+ - spec/concurrency_lock_spec.rb
159
+ - spec/berater_spec.rb
160
+ - spec/inhibitor_spec.rb
161
+ - spec/unlimiter_spec.rb