berater 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/berater/concurrency_limiter.rb +14 -9
- data/lib/berater/limiter.rb +7 -3
- data/lib/berater/lock.rb +3 -4
- data/lib/berater/rate_limiter.rb +14 -10
- data/lib/berater/rspec.rb +1 -1
- data/lib/berater/rspec/matchers.rb +68 -47
- data/lib/berater/unlimiter.rb +5 -1
- data/lib/berater/utils.rb +4 -4
- data/lib/berater/version.rb +1 -1
- data/spec/concurrency_limiter_spec.rb +21 -4
- data/spec/matchers_spec.rb +69 -13
- data/spec/rate_limiter_spec.rb +55 -14
- data/spec/test_mode_spec.rb +6 -13
- data/spec/utils_spec.rb +30 -30
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87d3bdb61a68a8b69dd7cb285dafb174a403acf19ff7586f90ed8b7cb50b7f76
|
4
|
+
data.tar.gz: fff71dd4d8ae28baf7c504efdc27df71f03219b32c1a959dddfd3d035845d058
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9fb5c0c7ec100d2f50a3b6c6cfa449af052c1b78cff174b545e179c5fcc1324c2a5faafc90e7f7ec64ce200e47571d51b02eb56398d716c487abc7731c0cece
|
7
|
+
data.tar.gz: 148c566413d6593f229143e2871116b45947eae5b101d8bb2f59ce3bdfe89ed4ee1b7aaedfcaedbc80eef8a25a14fab83ad5e540172c41f31f27d7f296161235
|
@@ -14,7 +14,7 @@ module Berater
|
|
14
14
|
private def timeout=(timeout)
|
15
15
|
@timeout = timeout
|
16
16
|
timeout = 0 if timeout == Float::INFINITY
|
17
|
-
@
|
17
|
+
@timeout_msec = Berater::Utils.to_msec(timeout)
|
18
18
|
end
|
19
19
|
|
20
20
|
LUA_SCRIPT = Berater::LuaScript(<<~LUA
|
@@ -65,24 +65,29 @@ module Berater
|
|
65
65
|
|
66
66
|
def limit(capacity: nil, cost: 1, &block)
|
67
67
|
capacity ||= @capacity
|
68
|
-
# cost is Integer >= 0
|
69
68
|
|
70
|
-
#
|
71
|
-
|
69
|
+
# since fractional cost is not supported, capacity behaves like int
|
70
|
+
capacity = capacity.to_i
|
71
|
+
|
72
|
+
unless cost.is_a?(Integer) && cost >= 0
|
73
|
+
raise ArgumentError, "invalid cost: #{cost}"
|
74
|
+
end
|
75
|
+
|
76
|
+
# timestamp in milliseconds
|
77
|
+
ts = (Time.now.to_f * 10**3).to_i
|
72
78
|
|
73
79
|
count, *lock_ids = LUA_SCRIPT.eval(
|
74
80
|
redis,
|
75
81
|
[ cache_key(key), cache_key('lock_id') ],
|
76
|
-
[ capacity, ts, @
|
82
|
+
[ capacity, ts, @timeout_msec, cost ]
|
77
83
|
)
|
78
84
|
|
79
85
|
raise Incapacitated if lock_ids.empty?
|
80
86
|
|
81
|
-
if cost
|
82
|
-
|
83
|
-
else
|
84
|
-
lock = Lock.new(self, lock_ids[0], count, -> { release(lock_ids) })
|
87
|
+
release_fn = if cost > 0
|
88
|
+
proc { release(lock_ids) }
|
85
89
|
end
|
90
|
+
lock = Lock.new(capacity, count, release_fn)
|
86
91
|
|
87
92
|
yield_lock(lock, &block)
|
88
93
|
end
|
data/lib/berater/limiter.rb
CHANGED
@@ -47,11 +47,15 @@ module Berater
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def capacity=(capacity)
|
50
|
-
unless capacity.is_a?(
|
51
|
-
raise ArgumentError, "expected
|
50
|
+
unless capacity.is_a?(Numeric)
|
51
|
+
raise ArgumentError, "expected Numeric, found #{capacity.class}"
|
52
52
|
end
|
53
53
|
|
54
|
-
|
54
|
+
if capacity == Float::INFINITY
|
55
|
+
raise ArgumentError, 'infinite capacity not supported, use Unlimiter'
|
56
|
+
end
|
57
|
+
|
58
|
+
raise ArgumentError, 'capacity must be >= 0' unless capacity >= 0
|
55
59
|
|
56
60
|
@capacity = capacity
|
57
61
|
end
|
data/lib/berater/lock.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
module Berater
|
2
2
|
class Lock
|
3
3
|
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :capacity, :contention
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
@
|
8
|
-
@id = id
|
6
|
+
def initialize(capacity, contention, release_fn = nil)
|
7
|
+
@capacity = capacity
|
9
8
|
@contention = contention
|
10
9
|
@locked_at = Time.now
|
11
10
|
@release_fn = release_fn
|
data/lib/berater/rate_limiter.rb
CHANGED
@@ -7,12 +7,16 @@ module Berater
|
|
7
7
|
|
8
8
|
def initialize(key, capacity, interval, **opts)
|
9
9
|
self.interval = interval
|
10
|
-
super(key, capacity, @
|
10
|
+
super(key, capacity, @interval_msec, **opts)
|
11
11
|
end
|
12
12
|
|
13
13
|
private def interval=(interval)
|
14
14
|
@interval = interval
|
15
|
-
@
|
15
|
+
@interval_msec = Berater::Utils.to_msec(interval)
|
16
|
+
|
17
|
+
unless @interval_msec > 0
|
18
|
+
raise ArgumentError, 'interval must be > 0'
|
19
|
+
end
|
16
20
|
end
|
17
21
|
|
18
22
|
LUA_SCRIPT = Berater::LuaScript(<<~LUA
|
@@ -20,11 +24,11 @@ module Berater
|
|
20
24
|
local ts_key = KEYS[2]
|
21
25
|
local ts = tonumber(ARGV[1])
|
22
26
|
local capacity = tonumber(ARGV[2])
|
23
|
-
local
|
27
|
+
local interval_msec = tonumber(ARGV[3])
|
24
28
|
local cost = tonumber(ARGV[4])
|
25
29
|
local count = 0
|
26
30
|
local allowed
|
27
|
-
local
|
31
|
+
local msec_per_drip = interval_msec / capacity
|
28
32
|
|
29
33
|
-- timestamp of last update
|
30
34
|
local last_ts = tonumber(redis.call('GET', ts_key))
|
@@ -33,7 +37,7 @@ module Berater
|
|
33
37
|
count = tonumber(redis.call('GET', key)) or 0
|
34
38
|
|
35
39
|
-- adjust for time passing
|
36
|
-
local drips = math.floor((ts - last_ts) /
|
40
|
+
local drips = math.floor((ts - last_ts) / msec_per_drip)
|
37
41
|
count = math.max(0, count - drips)
|
38
42
|
end
|
39
43
|
|
@@ -47,7 +51,7 @@ module Berater
|
|
47
51
|
count = count + cost
|
48
52
|
|
49
53
|
-- time for bucket to empty, in milliseconds
|
50
|
-
local ttl = math.ceil(
|
54
|
+
local ttl = math.ceil(count * msec_per_drip)
|
51
55
|
|
52
56
|
-- update count and last_ts, with expirations
|
53
57
|
redis.call('SET', key, count, 'PX', ttl)
|
@@ -62,18 +66,18 @@ module Berater
|
|
62
66
|
def limit(capacity: nil, cost: 1, &block)
|
63
67
|
capacity ||= @capacity
|
64
68
|
|
65
|
-
# timestamp in
|
66
|
-
ts = (Time.now.to_f * 10**
|
69
|
+
# timestamp in milliseconds
|
70
|
+
ts = (Time.now.to_f * 10**3).to_i
|
67
71
|
|
68
72
|
count, allowed = LUA_SCRIPT.eval(
|
69
73
|
redis,
|
70
74
|
[ cache_key(key), cache_key("#{key}-ts") ],
|
71
|
-
[ ts, capacity, @
|
75
|
+
[ ts, capacity, @interval_msec, cost ]
|
72
76
|
)
|
73
77
|
|
74
78
|
raise Overrated unless allowed
|
75
79
|
|
76
|
-
lock = Lock.new(
|
80
|
+
lock = Lock.new(capacity, count)
|
77
81
|
yield_lock(lock, &block)
|
78
82
|
end
|
79
83
|
|
data/lib/berater/rspec.rb
CHANGED
@@ -1,62 +1,83 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module Berater
|
2
|
+
module Matchers
|
3
|
+
class Overloaded
|
4
|
+
def initialize(type)
|
5
|
+
@type = type
|
6
|
+
end
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
def supports_block_expectations?
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
def matches?(obj)
|
13
|
+
begin
|
14
|
+
case obj
|
15
|
+
when Proc
|
16
|
+
# eg. expect { ... }.to be_overrated
|
17
|
+
res = obj.call
|
10
18
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
false
|
19
|
+
if res.is_a? Berater::Limiter
|
20
|
+
# eg. expect { Berater.new(...) }.to be_overloaded
|
21
|
+
@limiter = res
|
22
|
+
res.overloaded?
|
23
|
+
else
|
24
|
+
# eg. expect { Berater(...) }.to be_overloaded
|
25
|
+
# eg. expect { limiter.limit }.to be_overloaded
|
26
|
+
false
|
27
|
+
end
|
28
|
+
when Berater::Limiter
|
29
|
+
# eg. expect(Berater.new(...)).to be_overloaded
|
30
|
+
@limiter = obj
|
31
|
+
obj.overloaded?
|
25
32
|
end
|
26
|
-
|
27
|
-
|
28
|
-
obj.overloaded?
|
33
|
+
rescue @type
|
34
|
+
true
|
29
35
|
end
|
30
|
-
rescue @type
|
31
|
-
true
|
32
36
|
end
|
33
|
-
end
|
34
37
|
|
35
|
-
|
36
|
-
|
38
|
+
def description
|
39
|
+
if @limiter
|
40
|
+
"be #{verb}"
|
41
|
+
else
|
42
|
+
"raise #{@type}"
|
43
|
+
end
|
44
|
+
end
|
37
45
|
|
38
|
-
|
39
|
-
|
40
|
-
|
46
|
+
def failure_message
|
47
|
+
if @limiter
|
48
|
+
"expected to be #{verb}"
|
49
|
+
else
|
50
|
+
"expected #{@type} to be raised"
|
51
|
+
end
|
52
|
+
end
|
41
53
|
|
42
|
-
|
43
|
-
|
54
|
+
def failure_message_when_negated
|
55
|
+
if @limiter
|
56
|
+
"expected not to be #{verb}"
|
57
|
+
else
|
58
|
+
"did not expect #{@type} to be raised"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private def verb
|
63
|
+
@type.to_s.split('::')[-1].downcase
|
64
|
+
end
|
44
65
|
end
|
45
|
-
end
|
46
66
|
|
47
|
-
|
48
|
-
|
49
|
-
|
67
|
+
def be_overloaded
|
68
|
+
Overloaded.new(Berater::Overloaded)
|
69
|
+
end
|
50
70
|
|
51
|
-
|
52
|
-
|
53
|
-
|
71
|
+
def be_overrated
|
72
|
+
Overloaded.new(Berater::RateLimiter::Overrated)
|
73
|
+
end
|
54
74
|
|
55
|
-
|
56
|
-
|
57
|
-
|
75
|
+
def be_incapacitated
|
76
|
+
Overloaded.new(Berater::ConcurrencyLimiter::Incapacitated)
|
77
|
+
end
|
58
78
|
|
59
|
-
|
60
|
-
|
79
|
+
def be_inhibited
|
80
|
+
Overloaded.new(Berater::Inhibitor::Inhibited)
|
81
|
+
end
|
61
82
|
end
|
62
83
|
end
|
data/lib/berater/unlimiter.rb
CHANGED
@@ -6,12 +6,16 @@ module Berater
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def limit(**opts, &block)
|
9
|
-
yield_lock(Lock.new(
|
9
|
+
yield_lock(Lock.new(Float::INFINITY, 0), &block)
|
10
10
|
end
|
11
11
|
|
12
12
|
def overloaded?
|
13
13
|
false
|
14
14
|
end
|
15
15
|
|
16
|
+
protected def capacity=(*)
|
17
|
+
@capacity = Float::INFINITY
|
18
|
+
end
|
19
|
+
|
16
20
|
end
|
17
21
|
end
|
data/lib/berater/utils.rb
CHANGED
@@ -3,12 +3,12 @@ module Berater
|
|
3
3
|
extend self
|
4
4
|
|
5
5
|
refine Object do
|
6
|
-
def
|
7
|
-
Berater::Utils.
|
6
|
+
def to_msec
|
7
|
+
Berater::Utils.to_msec(self)
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
11
|
+
def to_msec(val)
|
12
12
|
res = val
|
13
13
|
|
14
14
|
if val.is_a? String
|
@@ -39,7 +39,7 @@ module Berater
|
|
39
39
|
raise ArgumentError, "infinite values not allowed"
|
40
40
|
end
|
41
41
|
|
42
|
-
(res * 10**
|
42
|
+
(res * 10**3).to_i
|
43
43
|
end
|
44
44
|
|
45
45
|
end
|
data/lib/berater/version.rb
CHANGED
@@ -23,6 +23,7 @@ describe Berater::ConcurrencyLimiter do
|
|
23
23
|
|
24
24
|
it { expect_capacity(0) }
|
25
25
|
it { expect_capacity(1) }
|
26
|
+
it { expect_capacity(1.5) }
|
26
27
|
it { expect_capacity(10_000) }
|
27
28
|
|
28
29
|
context 'with erroneous values' do
|
@@ -32,7 +33,6 @@ describe Berater::ConcurrencyLimiter do
|
|
32
33
|
end.to raise_error ArgumentError
|
33
34
|
end
|
34
35
|
|
35
|
-
it { expect_bad_capacity(0.5) }
|
36
36
|
it { expect_bad_capacity(-1) }
|
37
37
|
it { expect_bad_capacity('1') }
|
38
38
|
it { expect_bad_capacity(:one) }
|
@@ -42,16 +42,16 @@ describe Berater::ConcurrencyLimiter do
|
|
42
42
|
describe '#timeout' do
|
43
43
|
# see spec/utils_spec.rb
|
44
44
|
|
45
|
-
it 'saves the interval in original and
|
45
|
+
it 'saves the interval in original and millisecond format' do
|
46
46
|
limiter = described_class.new(:key, 1, timeout: 3)
|
47
47
|
expect(limiter.timeout).to be 3
|
48
|
-
expect(limiter.instance_variable_get(:@
|
48
|
+
expect(limiter.instance_variable_get(:@timeout_msec)).to be (3 * 10**3)
|
49
49
|
end
|
50
50
|
|
51
51
|
it 'handles infinity' do
|
52
52
|
limiter = described_class.new(:key, 1, timeout: Float::INFINITY)
|
53
53
|
expect(limiter.timeout).to be Float::INFINITY
|
54
|
-
expect(limiter.instance_variable_get(:@
|
54
|
+
expect(limiter.instance_variable_get(:@timeout_msec)).to be 0
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
@@ -88,6 +88,18 @@ describe Berater::ConcurrencyLimiter do
|
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
+
context 'when capacity is a Float' do
|
92
|
+
let(:limiter) { described_class.new(:key, 1.5) }
|
93
|
+
|
94
|
+
it 'still works' do
|
95
|
+
lock = limiter.limit
|
96
|
+
|
97
|
+
# since fractional cost is not supported
|
98
|
+
expect(lock.capacity).to be 1
|
99
|
+
expect(limiter).to be_incapacitated
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
91
103
|
it 'limit resets over time' do
|
92
104
|
2.times { limiter.limit }
|
93
105
|
expect(limiter).to be_incapacitated
|
@@ -149,6 +161,11 @@ describe Berater::ConcurrencyLimiter do
|
|
149
161
|
5.times { limiter.limit(capacity: 10) }
|
150
162
|
expect { limiter }.to be_incapacitated
|
151
163
|
end
|
164
|
+
|
165
|
+
it 'only allows positive, Integer values' do
|
166
|
+
expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
|
167
|
+
expect { limiter.limit(cost: 1.5) }.to raise_error(ArgumentError)
|
168
|
+
end
|
152
169
|
end
|
153
170
|
|
154
171
|
context 'with same key, different limiters' do
|
data/spec/matchers_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
describe
|
1
|
+
describe Berater::Matchers::Overloaded do
|
2
|
+
|
2
3
|
context 'Berater::Unlimiter' do
|
3
4
|
let(:limiter) { Berater.new(:key, :unlimited) }
|
4
5
|
|
@@ -16,12 +17,6 @@ describe 'be_overloaded' do
|
|
16
17
|
it { expect { limiter.limit }.not_to be_inhibited }
|
17
18
|
it { expect { limiter.limit }.not_to be_overrated }
|
18
19
|
it { expect { limiter.limit }.not_to be_incapacitated }
|
19
|
-
|
20
|
-
it 'catches false positives' do
|
21
|
-
expect {
|
22
|
-
expect { limiter }.to be_overloaded
|
23
|
-
}.to fail
|
24
|
-
end
|
25
20
|
end
|
26
21
|
|
27
22
|
context 'Berater::Inhibitor' do
|
@@ -35,12 +30,6 @@ describe 'be_overloaded' do
|
|
35
30
|
|
36
31
|
it { expect { limiter.limit }.to be_overloaded }
|
37
32
|
it { expect { limiter.limit }.to be_inhibited }
|
38
|
-
|
39
|
-
it 'catches false negatives' do
|
40
|
-
expect {
|
41
|
-
expect { limiter }.not_to be_overloaded
|
42
|
-
}.to fail
|
43
|
-
end
|
44
33
|
end
|
45
34
|
|
46
35
|
context 'Berater::RateLimiter' do
|
@@ -127,4 +116,71 @@ describe 'be_overloaded' do
|
|
127
116
|
end
|
128
117
|
end
|
129
118
|
end
|
119
|
+
|
120
|
+
context 'when matchers fail' do
|
121
|
+
let(:unlimiter) { Berater::Unlimiter.new }
|
122
|
+
let(:inhibitor) { Berater::Inhibitor.new }
|
123
|
+
|
124
|
+
it 'catches false negatives' do
|
125
|
+
expect {
|
126
|
+
expect(unlimiter).to be_overloaded
|
127
|
+
}.to fail_including('expected to be overloaded')
|
128
|
+
|
129
|
+
expect {
|
130
|
+
expect { unlimiter }.to be_overloaded
|
131
|
+
}.to fail_including('expected to be overloaded')
|
132
|
+
|
133
|
+
expect {
|
134
|
+
expect { unlimiter.limit }.to be_overloaded
|
135
|
+
}.to fail_including("expected #{Berater::Overloaded} to be raised")
|
136
|
+
|
137
|
+
expect {
|
138
|
+
expect { 123 }.to be_overloaded
|
139
|
+
}.to fail_including("expected #{Berater::Overloaded} to be raised")
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'catches false positives' do
|
143
|
+
expect {
|
144
|
+
expect(inhibitor).not_to be_overloaded
|
145
|
+
}.to fail_including('expected not to be overloaded')
|
146
|
+
|
147
|
+
expect {
|
148
|
+
expect { inhibitor }.not_to be_overloaded
|
149
|
+
}.to fail_including('expected not to be overloaded')
|
150
|
+
|
151
|
+
expect {
|
152
|
+
expect { inhibitor.limit }.not_to be_overloaded
|
153
|
+
}.to fail_including("did not expect #{Berater::Overloaded} to be raised")
|
154
|
+
|
155
|
+
expect {
|
156
|
+
expect { raise Berater::Overloaded }.not_to be_overloaded
|
157
|
+
}.to fail_including("did not expect #{Berater::Overloaded} to be raised")
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'supports different verbs' do
|
161
|
+
expect {
|
162
|
+
expect { unlimiter }.to be_overrated
|
163
|
+
}.to fail_including('expected to be overrated')
|
164
|
+
|
165
|
+
expect {
|
166
|
+
expect { unlimiter }.to be_incapacitated
|
167
|
+
}.to fail_including('expected to be incapacitated')
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'supports different exceptions' do
|
171
|
+
expect {
|
172
|
+
expect { 123 }.to be_overrated
|
173
|
+
}.to fail_including(
|
174
|
+
"expected #{Berater::RateLimiter::Overrated} to be raised"
|
175
|
+
)
|
176
|
+
|
177
|
+
expect {
|
178
|
+
expect {
|
179
|
+
raise Berater::ConcurrencyLimiter::Incapacitated
|
180
|
+
}.not_to be_incapacitated
|
181
|
+
}.to fail_including(
|
182
|
+
"did not expect #{Berater::ConcurrencyLimiter::Incapacitated} to be raised"
|
183
|
+
)
|
184
|
+
end
|
185
|
+
end
|
130
186
|
end
|
data/spec/rate_limiter_spec.rb
CHANGED
@@ -23,6 +23,7 @@ describe Berater::RateLimiter do
|
|
23
23
|
|
24
24
|
it { expect_capacity(0) }
|
25
25
|
it { expect_capacity(1) }
|
26
|
+
it { expect_capacity(1.5) }
|
26
27
|
it { expect_capacity(100) }
|
27
28
|
|
28
29
|
context 'with erroneous values' do
|
@@ -32,7 +33,6 @@ describe Berater::RateLimiter do
|
|
32
33
|
end.to raise_error ArgumentError
|
33
34
|
end
|
34
35
|
|
35
|
-
it { expect_bad_capacity(0.5) }
|
36
36
|
it { expect_bad_capacity(-1) }
|
37
37
|
it { expect_bad_capacity('1') }
|
38
38
|
it { expect_bad_capacity(:one) }
|
@@ -44,9 +44,19 @@ describe Berater::RateLimiter do
|
|
44
44
|
|
45
45
|
subject { described_class.new(:key, 1, :second) }
|
46
46
|
|
47
|
-
it 'saves the interval in original and
|
47
|
+
it 'saves the interval in original and millisecond format' do
|
48
48
|
expect(subject.interval).to be :second
|
49
|
-
expect(subject.instance_variable_get(:@
|
49
|
+
expect(subject.instance_variable_get(:@interval_msec)).to be 10**3
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'must be > 0' do
|
53
|
+
expect {
|
54
|
+
described_class.new(:key, 1, 0)
|
55
|
+
}.to raise_error(ArgumentError)
|
56
|
+
|
57
|
+
expect {
|
58
|
+
described_class.new(:key, 1, -1)
|
59
|
+
}.to raise_error(ArgumentError)
|
50
60
|
end
|
51
61
|
end
|
52
62
|
|
@@ -68,26 +78,57 @@ describe Berater::RateLimiter do
|
|
68
78
|
expect(limiter).to be_overrated
|
69
79
|
end
|
70
80
|
|
71
|
-
it 'limit
|
81
|
+
it 'resets limit over time' do
|
72
82
|
3.times { limiter.limit }
|
73
83
|
expect(limiter).to be_overrated
|
74
84
|
|
75
|
-
# travel forward to just before the count decrements
|
76
|
-
Timecop.freeze(0.333)
|
77
|
-
expect(limiter).to be_overrated
|
78
|
-
|
79
|
-
# traveling one more millisecond will decrement the count
|
80
|
-
Timecop.freeze(0.001)
|
81
|
-
limiter.limit
|
82
|
-
expect(limiter).to be_overrated
|
83
|
-
|
84
|
-
# traveling 1 second will reset the count
|
85
85
|
Timecop.freeze(1)
|
86
86
|
|
87
87
|
3.times { limiter.limit }
|
88
88
|
expect(limiter).to be_overrated
|
89
89
|
end
|
90
90
|
|
91
|
+
context 'with millisecond precision' do
|
92
|
+
it 'resets limit over time' do
|
93
|
+
3.times { limiter.limit }
|
94
|
+
expect(limiter).to be_overrated
|
95
|
+
|
96
|
+
# travel forward to just before the count decrements
|
97
|
+
Timecop.freeze(0.333)
|
98
|
+
expect(limiter).to be_overrated
|
99
|
+
|
100
|
+
# traveling one more millisecond will decrement the count
|
101
|
+
Timecop.freeze(0.001)
|
102
|
+
limiter.limit
|
103
|
+
expect(limiter).to be_overrated
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'works when drip rate is < 1 per millisecond' do
|
107
|
+
limiter = described_class.new(:key, 2_000, :second)
|
108
|
+
|
109
|
+
limiter.capacity.times { limiter.limit }
|
110
|
+
expect(limiter).to be_overrated
|
111
|
+
|
112
|
+
Timecop.freeze(0.001)
|
113
|
+
expect(limiter).not_to be_overrated
|
114
|
+
|
115
|
+
2.times { limiter.limit }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'when capacity is a Float' do
|
120
|
+
let(:limiter) { described_class.new(:key, 1.5, :second) }
|
121
|
+
|
122
|
+
it 'still works' do
|
123
|
+
limiter.limit
|
124
|
+
expect(limiter).not_to be_overrated
|
125
|
+
|
126
|
+
expect { limiter.limit }.to be_overrated
|
127
|
+
|
128
|
+
limiter.limit(cost: 0.5)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
91
132
|
it 'accepts a dynamic capacity' do
|
92
133
|
limiter = described_class.new(:key, 1, :second)
|
93
134
|
|
data/spec/test_mode_spec.rb
CHANGED
@@ -172,20 +172,11 @@ describe Berater::TestMode, order: :defined do
|
|
172
172
|
describe 'ConcurrencyLimiter' do
|
173
173
|
subject { Berater::ConcurrencyLimiter.new(:key, 1) }
|
174
174
|
|
175
|
-
shared_examples 'a ConcurrencyLimiter' do
|
176
|
-
it { expect(subject).to be_a Berater::ConcurrencyLimiter }
|
177
|
-
|
178
|
-
it 'checks arguments' do
|
179
|
-
expect {
|
180
|
-
Berater::ConcurrencyLimiter.new(:key, 1.0)
|
181
|
-
}.to raise_error(ArgumentError)
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
175
|
context 'when test_mode = nil' do
|
186
176
|
before { Berater.test_mode = nil }
|
187
177
|
|
188
|
-
|
178
|
+
it { is_expected.to be_a Berater::ConcurrencyLimiter }
|
179
|
+
|
189
180
|
it_behaves_like 'it is not overloaded'
|
190
181
|
|
191
182
|
it 'works per usual' do
|
@@ -198,14 +189,16 @@ describe Berater::TestMode, order: :defined do
|
|
198
189
|
context 'when test_mode = :pass' do
|
199
190
|
before { Berater.test_mode = :pass }
|
200
191
|
|
201
|
-
|
192
|
+
it { is_expected.to be_a Berater::ConcurrencyLimiter }
|
193
|
+
|
202
194
|
it_behaves_like 'it always works, without redis'
|
203
195
|
end
|
204
196
|
|
205
197
|
context 'when test_mode = :fail' do
|
206
198
|
before { Berater.test_mode = :fail }
|
207
199
|
|
208
|
-
|
200
|
+
it { is_expected.to be_a Berater::ConcurrencyLimiter }
|
201
|
+
|
209
202
|
it_behaves_like 'it never works, without redis'
|
210
203
|
end
|
211
204
|
end
|
data/spec/utils_spec.rb
CHANGED
@@ -1,63 +1,63 @@
|
|
1
1
|
describe Berater::Utils do
|
2
2
|
using Berater::Utils
|
3
3
|
|
4
|
-
describe '.
|
4
|
+
describe '.to_msec' do
|
5
5
|
def f(val)
|
6
|
-
(val * 10**
|
6
|
+
(val * 10**3).to_i
|
7
7
|
end
|
8
8
|
|
9
9
|
it 'works with integers' do
|
10
|
-
expect(0.
|
11
|
-
expect(3.
|
10
|
+
expect(0.to_msec).to be f(0)
|
11
|
+
expect(3.to_msec).to be f(3)
|
12
12
|
end
|
13
13
|
|
14
14
|
it 'works with floats' do
|
15
|
-
expect(0.1.
|
16
|
-
expect(3.0.
|
15
|
+
expect(0.1.to_msec).to be f(0.1)
|
16
|
+
expect(3.0.to_msec).to be f(3)
|
17
17
|
end
|
18
18
|
|
19
|
-
it '
|
20
|
-
expect(0.123456.
|
21
|
-
expect(123456.654321.
|
19
|
+
it 'truncates excessive precision' do
|
20
|
+
expect(0.123456.to_msec).to be 123
|
21
|
+
expect(123456.654321.to_msec).to be 123456654
|
22
22
|
end
|
23
23
|
|
24
24
|
it 'works with symbols that are keywords' do
|
25
|
-
expect(:sec.
|
26
|
-
expect(:second.
|
27
|
-
expect(:seconds.
|
25
|
+
expect(:sec.to_msec).to be f(1)
|
26
|
+
expect(:second.to_msec).to be f(1)
|
27
|
+
expect(:seconds.to_msec).to be f(1)
|
28
28
|
|
29
|
-
expect(:min.
|
30
|
-
expect(:minute.
|
31
|
-
expect(:minutes.
|
29
|
+
expect(:min.to_msec).to be f(60)
|
30
|
+
expect(:minute.to_msec).to be f(60)
|
31
|
+
expect(:minutes.to_msec).to be f(60)
|
32
32
|
|
33
|
-
expect(:hour.
|
34
|
-
expect(:hours.
|
33
|
+
expect(:hour.to_msec).to be f(60 * 60)
|
34
|
+
expect(:hours.to_msec).to be f(60 * 60)
|
35
35
|
end
|
36
36
|
|
37
37
|
it 'works with strings that are keywords' do
|
38
|
-
expect('sec'.
|
39
|
-
expect('second'.
|
40
|
-
expect('seconds'.
|
38
|
+
expect('sec'.to_msec).to be f(1)
|
39
|
+
expect('second'.to_msec).to be f(1)
|
40
|
+
expect('seconds'.to_msec).to be f(1)
|
41
41
|
|
42
|
-
expect('min'.
|
43
|
-
expect('minute'.
|
44
|
-
expect('minutes'.
|
42
|
+
expect('min'.to_msec).to be f(60)
|
43
|
+
expect('minute'.to_msec).to be f(60)
|
44
|
+
expect('minutes'.to_msec).to be f(60)
|
45
45
|
|
46
|
-
expect('hour'.
|
47
|
-
expect('hours'.
|
46
|
+
expect('hour'.to_msec).to be f(60 * 60)
|
47
|
+
expect('hours'.to_msec).to be f(60 * 60)
|
48
48
|
end
|
49
49
|
|
50
50
|
it 'works with strings that are numeric' do
|
51
|
-
expect('0'.
|
52
|
-
expect('3'.
|
51
|
+
expect('0'.to_msec).to be f(0)
|
52
|
+
expect('3'.to_msec).to be f(3)
|
53
53
|
|
54
|
-
expect('0.1'.
|
55
|
-
expect('3.0'.
|
54
|
+
expect('0.1'.to_msec).to be f(0.1)
|
55
|
+
expect('3.0'.to_msec).to be f(3)
|
56
56
|
end
|
57
57
|
|
58
58
|
context 'with erroneous values' do
|
59
59
|
def e(val)
|
60
|
-
expect { val.
|
60
|
+
expect { val.to_msec }.to raise_error(ArgumentError)
|
61
61
|
end
|
62
62
|
|
63
63
|
it 'rejects negative numbers' do
|
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.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Pepper
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|