berater 0.5.0 → 0.7.1
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.rb +10 -7
- data/lib/berater/concurrency_limiter.rb +18 -27
- data/lib/berater/dsl.rb +8 -8
- data/lib/berater/inhibitor.rb +2 -9
- data/lib/berater/limiter.rb +44 -20
- data/lib/berater/lock.rb +3 -4
- data/lib/berater/rate_limiter.rb +43 -40
- data/lib/berater/rspec.rb +2 -2
- data/lib/berater/rspec/matchers.rb +37 -38
- data/lib/berater/test_mode.rb +13 -29
- data/lib/berater/unlimiter.rb +6 -4
- data/lib/berater/utils.rb +4 -4
- data/lib/berater/version.rb +1 -1
- data/spec/berater_spec.rb +9 -9
- data/spec/concurrency_limiter_spec.rb +82 -55
- data/spec/dsl_refinement_spec.rb +0 -12
- data/spec/dsl_spec.rb +5 -17
- data/spec/limiter_spec.rb +39 -3
- data/spec/matchers_spec.rb +64 -72
- data/spec/rate_limiter_spec.rb +134 -44
- data/spec/riddle_spec.rb +6 -2
- data/spec/test_mode_spec.rb +40 -111
- data/spec/utils_spec.rb +30 -30
- metadata +2 -2
data/lib/berater/rspec.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
require 'berater'
|
2
2
|
require 'berater/rspec/matchers'
|
3
3
|
require 'berater/test_mode'
|
4
|
-
require 'rspec'
|
4
|
+
require 'rspec/core'
|
5
5
|
|
6
6
|
RSpec.configure do |config|
|
7
|
-
config.include(
|
7
|
+
config.include(Berater::Matchers)
|
8
8
|
|
9
9
|
config.after do
|
10
10
|
Berater.expunge rescue nil
|
@@ -1,23 +1,20 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
def supports_block_expectations?
|
8
|
-
true
|
9
|
-
end
|
1
|
+
module Berater
|
2
|
+
module Matchers
|
3
|
+
class Overloaded
|
4
|
+
def supports_block_expectations?
|
5
|
+
true
|
6
|
+
end
|
10
7
|
|
11
|
-
|
12
|
-
begin
|
8
|
+
def matches?(obj)
|
13
9
|
case obj
|
14
10
|
when Proc
|
15
|
-
# eg. expect { ... }.to
|
11
|
+
# eg. expect { ... }.to be_overloaded
|
16
12
|
res = obj.call
|
17
13
|
|
18
14
|
if res.is_a? Berater::Limiter
|
19
15
|
# eg. expect { Berater.new(...) }.to be_overloaded
|
20
|
-
res
|
16
|
+
@limiter = res
|
17
|
+
@limiter.utilization >= 1
|
21
18
|
else
|
22
19
|
# eg. expect { Berater(...) }.to be_overloaded
|
23
20
|
# eg. expect { limiter.limit }.to be_overloaded
|
@@ -25,38 +22,40 @@ module BeraterMatchers
|
|
25
22
|
end
|
26
23
|
when Berater::Limiter
|
27
24
|
# eg. expect(Berater.new(...)).to be_overloaded
|
28
|
-
obj
|
25
|
+
@limiter = obj
|
26
|
+
@limiter.utilization >= 1
|
29
27
|
end
|
30
|
-
rescue
|
28
|
+
rescue Berater::Overloaded
|
31
29
|
true
|
32
30
|
end
|
33
|
-
end
|
34
31
|
|
35
|
-
|
36
|
-
|
32
|
+
def description
|
33
|
+
if @limiter
|
34
|
+
"be overloaded"
|
35
|
+
else
|
36
|
+
"raise #{Berater::Overloaded}"
|
37
|
+
end
|
38
|
+
end
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
def failure_message
|
41
|
+
if @limiter
|
42
|
+
"expected to be overloaded"
|
43
|
+
else
|
44
|
+
"expected #{Berater::Overloaded} to be raised"
|
45
|
+
end
|
46
|
+
end
|
41
47
|
|
42
|
-
|
43
|
-
|
48
|
+
def failure_message_when_negated
|
49
|
+
if @limiter
|
50
|
+
"expected not to be overloaded"
|
51
|
+
else
|
52
|
+
"did not expect #{Berater::Overloaded} to be raised"
|
53
|
+
end
|
54
|
+
end
|
44
55
|
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def be_overloaded
|
48
|
-
Overloaded.new(Berater::Overloaded)
|
49
|
-
end
|
50
56
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
def be_incapacitated
|
56
|
-
Overloaded.new(Berater::ConcurrencyLimiter::Incapacitated)
|
57
|
-
end
|
58
|
-
|
59
|
-
def be_inhibited
|
60
|
-
Overloaded.new(Berater::Inhibitor::Inhibited)
|
57
|
+
def be_overloaded
|
58
|
+
Overloaded.new
|
59
|
+
end
|
61
60
|
end
|
62
61
|
end
|
data/lib/berater/test_mode.rb
CHANGED
@@ -11,42 +11,26 @@ module Berater
|
|
11
11
|
end
|
12
12
|
|
13
13
|
@test_mode = mode
|
14
|
-
|
15
|
-
# overload class methods
|
16
|
-
unless Berater::Limiter.singleton_class.ancestors.include?(TestMode)
|
17
|
-
Berater::Limiter.singleton_class.prepend(TestMode)
|
18
|
-
end
|
19
14
|
end
|
20
15
|
|
21
16
|
module TestMode
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
# chose a stub class with desired behavior
|
26
|
-
stub_klass = case Berater.test_mode
|
17
|
+
def acquire_lock(*)
|
18
|
+
case Berater.test_mode
|
27
19
|
when :pass
|
28
|
-
|
20
|
+
Lock.new(Float::INFINITY, 0)
|
29
21
|
when :fail
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
# don't stub self
|
34
|
-
return super if self < stub_klass
|
35
|
-
|
36
|
-
# swap out limit and overloaded? methods with stub
|
37
|
-
super.tap do |instance|
|
38
|
-
stub = stub_klass.allocate
|
39
|
-
stub.send(:initialize, *args, **opts)
|
40
|
-
|
41
|
-
instance.define_singleton_method(:limit) do |**opts, &block|
|
42
|
-
stub.limit(**opts, &block)
|
43
|
-
end
|
44
|
-
|
45
|
-
instance.define_singleton_method(:overloaded?) do
|
46
|
-
stub.overloaded?
|
47
|
-
end
|
22
|
+
raise Overloaded
|
23
|
+
else
|
24
|
+
super
|
48
25
|
end
|
49
26
|
end
|
50
27
|
end
|
51
28
|
|
52
29
|
end
|
30
|
+
|
31
|
+
# stub each Limiter subclass
|
32
|
+
ObjectSpace.each_object(Class).each do |klass|
|
33
|
+
next unless klass < Berater::Limiter
|
34
|
+
|
35
|
+
klass.prepend(Berater::TestMode)
|
36
|
+
end
|
data/lib/berater/unlimiter.rb
CHANGED
@@ -5,12 +5,14 @@ module Berater
|
|
5
5
|
super(key, Float::INFINITY, **opts)
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
protected
|
9
|
+
|
10
|
+
def capacity=(*)
|
11
|
+
@capacity = Float::INFINITY
|
10
12
|
end
|
11
13
|
|
12
|
-
def
|
13
|
-
|
14
|
+
def acquire_lock(*)
|
15
|
+
Lock.new(Float::INFINITY, 0)
|
14
16
|
end
|
15
17
|
|
16
18
|
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
data/spec/berater_spec.rb
CHANGED
@@ -25,7 +25,7 @@ describe Berater do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
describe '.new' do
|
28
|
-
context '
|
28
|
+
context 'Unlimiter mode' do
|
29
29
|
let(:limiter) { Berater.new(:key, Float::INFINITY) }
|
30
30
|
|
31
31
|
it 'instantiates an Unlimiter' do
|
@@ -44,7 +44,7 @@ describe Berater do
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
context '
|
47
|
+
context 'Inhibitor mode' do
|
48
48
|
let(:limiter) { Berater.new(:key, 0) }
|
49
49
|
|
50
50
|
it 'instantiates an Inhibitor' do
|
@@ -64,7 +64,7 @@ describe Berater do
|
|
64
64
|
end
|
65
65
|
|
66
66
|
context 'rate mode' do
|
67
|
-
let(:limiter) { Berater.new(:key, 1, :second) }
|
67
|
+
let(:limiter) { Berater.new(:key, 1, interval: :second) }
|
68
68
|
|
69
69
|
it 'instantiates a RateLimiter' do
|
70
70
|
expect(limiter).to be_a Berater::RateLimiter
|
@@ -77,7 +77,7 @@ describe Berater do
|
|
77
77
|
|
78
78
|
it 'accepts options' do
|
79
79
|
redis = double('Redis')
|
80
|
-
limiter = Berater.new(:key, 1, :second, redis: redis)
|
80
|
+
limiter = Berater.new(:key, 1, interval: :second, redis: redis)
|
81
81
|
expect(limiter.redis).to be redis
|
82
82
|
end
|
83
83
|
end
|
@@ -103,20 +103,20 @@ describe Berater do
|
|
103
103
|
end
|
104
104
|
|
105
105
|
describe 'Berater() - convenience method' do
|
106
|
-
RSpec.shared_examples 'test convenience' do |klass,
|
106
|
+
RSpec.shared_examples 'test convenience' do |klass, capacity, **opts|
|
107
107
|
it 'creates a limiter' do
|
108
|
-
limiter = Berater(:key,
|
108
|
+
limiter = Berater(:key, capacity, **opts)
|
109
109
|
expect(limiter).to be_a klass
|
110
110
|
end
|
111
111
|
|
112
112
|
context 'with a block' do
|
113
113
|
it 'creates a limiter and calls limit' do
|
114
|
-
limiter = Berater(:key,
|
114
|
+
limiter = Berater(:key, capacity, **opts)
|
115
115
|
expect(klass).to receive(:new).and_return(limiter)
|
116
116
|
expect(limiter).to receive(:limit).and_call_original
|
117
117
|
|
118
118
|
begin
|
119
|
-
res = Berater(:key,
|
119
|
+
res = Berater(:key, capacity, **opts) { true }
|
120
120
|
expect(res).to be true
|
121
121
|
rescue Berater::Overloaded
|
122
122
|
expect(klass).to be Berater::Inhibitor
|
@@ -127,7 +127,7 @@ describe Berater do
|
|
127
127
|
|
128
128
|
include_examples 'test convenience', Berater::Unlimiter, Float::INFINITY
|
129
129
|
include_examples 'test convenience', Berater::Inhibitor, 0
|
130
|
-
include_examples 'test convenience', Berater::RateLimiter, 1, :second
|
130
|
+
include_examples 'test convenience', Berater::RateLimiter, 1, interval: :second
|
131
131
|
include_examples 'test convenience', Berater::ConcurrencyLimiter, 1
|
132
132
|
end
|
133
133
|
|
@@ -18,11 +18,12 @@ describe Berater::ConcurrencyLimiter do
|
|
18
18
|
describe '#capacity' do
|
19
19
|
def expect_capacity(capacity)
|
20
20
|
limiter = described_class.new(:key, capacity)
|
21
|
-
expect(limiter.capacity).to eq capacity
|
21
|
+
expect(limiter.capacity).to eq capacity.to_i
|
22
22
|
end
|
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
|
|
@@ -77,77 +77,104 @@ describe Berater::ConcurrencyLimiter do
|
|
77
77
|
expect(limiter.limit).to be_a Berater::Lock
|
78
78
|
expect(limiter.limit).to be_a Berater::Lock
|
79
79
|
|
80
|
-
expect(limiter).to
|
80
|
+
expect(limiter).to be_overloaded
|
81
81
|
end
|
82
82
|
|
83
83
|
context 'with capacity 0' do
|
84
84
|
let(:limiter) { described_class.new(:key, 0) }
|
85
85
|
|
86
86
|
it 'always fails' do
|
87
|
-
expect(limiter).to
|
87
|
+
expect(limiter).to be_overloaded
|
88
|
+
end
|
89
|
+
end
|
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_overloaded
|
88
100
|
end
|
89
101
|
end
|
90
102
|
|
91
103
|
it 'limit resets over time' do
|
92
104
|
2.times { limiter.limit }
|
93
|
-
expect(limiter).to
|
105
|
+
expect(limiter).to be_overloaded
|
94
106
|
|
95
107
|
Timecop.freeze(30)
|
96
108
|
|
97
109
|
2.times { limiter.limit }
|
98
|
-
expect(limiter).to
|
110
|
+
expect(limiter).to be_overloaded
|
99
111
|
end
|
100
112
|
|
101
113
|
it 'limit resets with millisecond precision' do
|
102
114
|
2.times { limiter.limit }
|
103
|
-
expect(limiter).to
|
115
|
+
expect(limiter).to be_overloaded
|
104
116
|
|
105
117
|
# travel forward to just before first lock times out
|
106
118
|
Timecop.freeze(29.999)
|
107
|
-
expect(limiter).to
|
119
|
+
expect(limiter).to be_overloaded
|
108
120
|
|
109
121
|
# traveling one more millisecond will decrement the count
|
110
122
|
Timecop.freeze(0.001)
|
111
123
|
2.times { limiter.limit }
|
112
|
-
expect(limiter).to
|
124
|
+
expect(limiter).to be_overloaded
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'accepts a dynamic capacity' do
|
128
|
+
expect { limiter.limit(capacity: 0) }.to be_overloaded
|
129
|
+
5.times { limiter.limit(capacity: 10) }
|
130
|
+
expect { limiter }.to be_overloaded
|
113
131
|
end
|
114
132
|
|
115
133
|
context 'with cost parameter' do
|
116
|
-
it { expect { limiter.limit(cost: 4) }.to
|
134
|
+
it { expect { limiter.limit(cost: 4) }.to be_overloaded }
|
117
135
|
|
118
136
|
it 'works within limit' do
|
119
137
|
limiter.limit(cost: 2)
|
120
|
-
expect(limiter).to
|
138
|
+
expect(limiter).to be_overloaded
|
121
139
|
end
|
122
140
|
|
123
141
|
it 'releases full cost' do
|
124
142
|
lock = limiter.limit(cost: 2)
|
125
|
-
expect(limiter).to
|
143
|
+
expect(limiter).to be_overloaded
|
126
144
|
|
127
145
|
lock.release
|
128
|
-
expect(limiter).not_to
|
146
|
+
expect(limiter).not_to be_overloaded
|
129
147
|
|
130
148
|
lock = limiter.limit(cost: 2)
|
131
|
-
expect(limiter).to
|
149
|
+
expect(limiter).to be_overloaded
|
132
150
|
end
|
133
151
|
|
134
152
|
it 'respects timeout' do
|
135
153
|
limiter.limit(cost: 2)
|
136
|
-
expect(limiter).to
|
154
|
+
expect(limiter).to be_overloaded
|
137
155
|
|
138
156
|
Timecop.freeze(30)
|
139
|
-
expect(limiter).not_to
|
157
|
+
expect(limiter).not_to be_overloaded
|
140
158
|
|
141
159
|
limiter.limit(cost: 2)
|
142
|
-
expect(limiter).to
|
160
|
+
expect(limiter).to be_overloaded
|
143
161
|
end
|
144
162
|
|
145
|
-
|
146
|
-
|
163
|
+
context 'with fractional costs' do
|
164
|
+
it 'rounds up' do
|
165
|
+
limiter.limit(cost: 1.5)
|
166
|
+
expect(limiter).to be_overloaded
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'accumulates correctly' do
|
170
|
+
limiter.limit(cost: 0.5) # => 1
|
171
|
+
limiter.limit(cost: 0.7) # => 2
|
172
|
+
expect(limiter).to be_overloaded
|
173
|
+
end
|
174
|
+
end
|
147
175
|
|
148
|
-
|
149
|
-
|
150
|
-
expect { limiter }.to be_incapacitated
|
176
|
+
it 'only allows positive values' do
|
177
|
+
expect { limiter.limit(cost: -1) }.to raise_error(ArgumentError)
|
151
178
|
end
|
152
179
|
end
|
153
180
|
|
@@ -160,8 +187,8 @@ describe Berater::ConcurrencyLimiter do
|
|
160
187
|
it 'works as expected' do
|
161
188
|
expect(limiter_one.limit).to be_a Berater::Lock
|
162
189
|
|
163
|
-
expect(limiter_one).to
|
164
|
-
expect(limiter_two).to
|
190
|
+
expect(limiter_one).to be_overloaded
|
191
|
+
expect(limiter_two).to be_overloaded
|
165
192
|
end
|
166
193
|
end
|
167
194
|
|
@@ -175,22 +202,22 @@ describe Berater::ConcurrencyLimiter do
|
|
175
202
|
one_lock = limiter_one.limit
|
176
203
|
expect(one_lock).to be_a Berater::Lock
|
177
204
|
|
178
|
-
expect(limiter_one).to
|
179
|
-
expect(limiter_two).not_to
|
205
|
+
expect(limiter_one).to be_overloaded
|
206
|
+
expect(limiter_two).not_to be_overloaded
|
180
207
|
|
181
208
|
two_lock = limiter_two.limit
|
182
209
|
expect(two_lock).to be_a Berater::Lock
|
183
210
|
|
184
|
-
expect(limiter_one).to
|
185
|
-
expect(limiter_two).to
|
211
|
+
expect(limiter_one).to be_overloaded
|
212
|
+
expect(limiter_two).to be_overloaded
|
186
213
|
|
187
214
|
one_lock.release
|
188
|
-
expect(limiter_one).to
|
189
|
-
expect(limiter_two).not_to
|
215
|
+
expect(limiter_one).to be_overloaded
|
216
|
+
expect(limiter_two).not_to be_overloaded
|
190
217
|
|
191
218
|
two_lock.release
|
192
|
-
expect(limiter_one).not_to
|
193
|
-
expect(limiter_two).not_to
|
219
|
+
expect(limiter_one).not_to be_overloaded
|
220
|
+
expect(limiter_two).not_to be_overloaded
|
194
221
|
end
|
195
222
|
end
|
196
223
|
|
@@ -199,41 +226,41 @@ describe Berater::ConcurrencyLimiter do
|
|
199
226
|
let(:limiter_two) { described_class.new(:two, 1) }
|
200
227
|
|
201
228
|
it 'works as expected' do
|
202
|
-
expect(limiter_one).not_to
|
203
|
-
expect(limiter_two).not_to
|
229
|
+
expect(limiter_one).not_to be_overloaded
|
230
|
+
expect(limiter_two).not_to be_overloaded
|
204
231
|
|
205
232
|
one_lock = limiter_one.limit
|
206
233
|
expect(one_lock).to be_a Berater::Lock
|
207
234
|
|
208
|
-
expect(limiter_one).to
|
209
|
-
expect(limiter_two).not_to
|
235
|
+
expect(limiter_one).to be_overloaded
|
236
|
+
expect(limiter_two).not_to be_overloaded
|
210
237
|
|
211
238
|
two_lock = limiter_two.limit
|
212
239
|
expect(two_lock).to be_a Berater::Lock
|
213
240
|
|
214
|
-
expect(limiter_one).to
|
215
|
-
expect(limiter_two).to
|
241
|
+
expect(limiter_one).to be_overloaded
|
242
|
+
expect(limiter_two).to be_overloaded
|
216
243
|
end
|
217
244
|
end
|
218
245
|
end
|
219
246
|
|
220
|
-
describe '#
|
221
|
-
let(:limiter) { described_class.new(:key,
|
247
|
+
describe '#utilization' do
|
248
|
+
let(:limiter) { described_class.new(:key, 10, timeout: 30) }
|
222
249
|
|
223
250
|
it 'works' do
|
224
|
-
expect(limiter.
|
225
|
-
lock = limiter.limit
|
226
|
-
expect(limiter.overloaded?).to be true
|
227
|
-
lock.release
|
228
|
-
expect(limiter.overloaded?).to be false
|
229
|
-
end
|
251
|
+
expect(limiter.utilization).to be 0.0
|
230
252
|
|
231
|
-
|
232
|
-
expect(limiter.
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
253
|
+
2.times { limiter.limit }
|
254
|
+
expect(limiter.utilization).to be 0.2
|
255
|
+
|
256
|
+
Timecop.freeze(15)
|
257
|
+
|
258
|
+
8.times { limiter.limit }
|
259
|
+
expect(limiter.utilization).to be 1.0
|
260
|
+
|
261
|
+
Timecop.freeze(15)
|
262
|
+
|
263
|
+
expect(limiter.utilization).to be 0.8
|
237
264
|
end
|
238
265
|
end
|
239
266
|
|