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 +5 -5
- data/lib/berater.rb +48 -16
- data/lib/berater/base_limiter.rb +34 -0
- data/lib/berater/concurrency_limiter.rb +149 -0
- data/lib/berater/inhibitor.rb +21 -0
- data/lib/berater/rate_limiter.rb +84 -0
- data/lib/berater/unlimiter.rb +19 -0
- data/lib/berater/version.rb +3 -0
- data/spec/berater_spec.rb +192 -0
- data/spec/concurrency_limiter_spec.rb +189 -0
- data/spec/concurrency_lock_spec.rb +34 -0
- data/spec/inhibitor_spec.rb +37 -0
- data/spec/matcher_spec.rb +118 -0
- data/spec/rate_limiter_spec.rb +166 -0
- data/spec/unlimiter_spec.rb +61 -0
- metadata +81 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f1a54a65428bf73c9a8840d95900215411a7320d87af5297cbc75039976e182c
|
4
|
+
data.tar.gz: 5804acf97ae46ab33bf8366438304a301d183fb0b8da48f4b872252769e79a54
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
10
|
+
extend self
|
3
11
|
|
4
|
-
|
12
|
+
Overloaded = BaseLimiter::Overloaded
|
5
13
|
|
6
|
-
|
7
|
-
|
8
|
-
|
14
|
+
MODES = {}
|
15
|
+
|
16
|
+
attr_accessor :redis, :mode
|
9
17
|
|
18
|
+
def configure
|
19
|
+
self.mode = :unlimited # default
|
10
20
|
|
11
|
-
|
12
|
-
|
21
|
+
yield self
|
22
|
+
end
|
13
23
|
|
14
|
-
|
15
|
-
|
24
|
+
def new(mode, *args, **opts)
|
25
|
+
klass = MODES[mode.to_sym]
|
16
26
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end.first
|
27
|
+
unless klass
|
28
|
+
raise ArgumentError, "invalid mode: #{mode}"
|
29
|
+
end
|
21
30
|
|
22
|
-
|
31
|
+
klass.new(*args, **opts)
|
32
|
+
end
|
23
33
|
|
24
|
-
|
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,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
|
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:
|
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:
|
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: '
|
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: '
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
70
|
+
name: rspec
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
|
-
- - "
|
73
|
+
- - ">="
|
60
74
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
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: '
|
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
|
-
|
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
|
-
|
96
|
-
|
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
|