berater 0.0.1 → 0.1.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
- 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