berater 0.5.0 → 0.6.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 +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
|