berater 0.13.1 → 0.15.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/limiter.rb +4 -9
- data/lib/berater/middleware/trace.rb +4 -2
- data/lib/berater/mutex.rb +28 -0
- data/lib/berater/rspec/matchers.rb +2 -2
- data/lib/berater/version.rb +1 -1
- data/lib/berater.rb +2 -6
- data/spec/concurrency_limiter_spec.rb +4 -4
- data/spec/middleware/trace_spec.rb +10 -0
- data/spec/mutex_spec.rb +145 -0
- data/spec/rate_limiter_spec.rb +4 -4
- data/spec/static_limiter_spec.rb +3 -3
- data/spec/test_mode_spec.rb +9 -0
- metadata +24 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 81f3c362a1360d2b3f3b4a2829b374dd38f0f9de6809737efe7d9bb44ffbd88c
|
4
|
+
data.tar.gz: cbad555e49e5ac86d63f99486139e6df13b9da4289832b2d8cf5034bd7140018
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed26b29a67a30db075ec18ab64698b6ae76dbe7d4d8aee64c290dc449a6867566ea3c8155eb6ec6c5fe440208c19345bfc02c30adcddb4356b591f784678df99
|
7
|
+
data.tar.gz: 56143daeb1069e5d1162bea25c9892f146009168f8ef42c23cdc78774e6148602ef40364423c94e5248d2cff99793e6e5ef6a676e918ec4cf23d2686a35b5927
|
data/lib/berater/limiter.rb
CHANGED
@@ -62,12 +62,12 @@ module Berater
|
|
62
62
|
lock = limit(cost: 0)
|
63
63
|
|
64
64
|
if lock.capacity == 0
|
65
|
-
|
65
|
+
100.0
|
66
66
|
else
|
67
|
-
lock.contention.to_f / lock.capacity
|
67
|
+
lock.contention.to_f / lock.capacity * 100
|
68
68
|
end
|
69
69
|
rescue Berater::Overloaded
|
70
|
-
|
70
|
+
100.0
|
71
71
|
end
|
72
72
|
|
73
73
|
def ==(other)
|
@@ -124,12 +124,7 @@ module Berater
|
|
124
124
|
# can only call via subclass
|
125
125
|
raise NoMethodError if self == Berater::Limiter
|
126
126
|
|
127
|
-
|
128
|
-
# avoid ruby 2 problems with empty hashes
|
129
|
-
super(*args)
|
130
|
-
else
|
131
|
-
super
|
132
|
-
end
|
127
|
+
super
|
133
128
|
end
|
134
129
|
|
135
130
|
def cache_key(key)
|
@@ -6,7 +6,9 @@ module Berater
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def call(limiter, **)
|
9
|
-
|
9
|
+
return yield unless tracer
|
10
|
+
|
11
|
+
tracer.trace('Berater') do |span|
|
10
12
|
begin
|
11
13
|
lock = yield
|
12
14
|
rescue Exception => error
|
@@ -32,7 +34,7 @@ module Berater
|
|
32
34
|
private
|
33
35
|
|
34
36
|
def tracer
|
35
|
-
@tracer || (defined?(Datadog
|
37
|
+
@tracer || (defined?(Datadog) && Datadog::Tracing)
|
36
38
|
end
|
37
39
|
end
|
38
40
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Berater
|
2
|
+
module Mutex
|
3
|
+
def self.included(base)
|
4
|
+
# add class methods
|
5
|
+
base.instance_eval do
|
6
|
+
def synchronize(subkey = nil, **opts, &block)
|
7
|
+
key = [ 'Mutex', name&.delete(':') || object_id, subkey ].compact.join(':')
|
8
|
+
|
9
|
+
Berater::ConcurrencyLimiter(key, 1, **mutex_options.merge(opts)) do
|
10
|
+
yield if block_given?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def mutex_options(**kwargs)
|
15
|
+
(@mutex_options ||= {}).update(kwargs)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def synchronize(...)
|
21
|
+
self.class.synchronize(...)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.extend_object(base)
|
25
|
+
included(base)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -14,7 +14,7 @@ module Berater
|
|
14
14
|
if res.is_a? Berater::Limiter
|
15
15
|
# eg. expect { Berater.new(...) }.to be_overloaded
|
16
16
|
@limiter = res
|
17
|
-
@limiter.utilization >=
|
17
|
+
@limiter.utilization >= 100
|
18
18
|
else
|
19
19
|
# eg. expect { Berater(...) }.to be_overloaded
|
20
20
|
# eg. expect { limiter.limit }.to be_overloaded
|
@@ -23,7 +23,7 @@ module Berater
|
|
23
23
|
when Berater::Limiter
|
24
24
|
# eg. expect(Berater.new(...)).to be_overloaded
|
25
25
|
@limiter = obj
|
26
|
-
@limiter.utilization >=
|
26
|
+
@limiter.utilization >= 100
|
27
27
|
end
|
28
28
|
rescue Berater::Overloaded
|
29
29
|
true
|
data/lib/berater/version.rb
CHANGED
data/lib/berater.rb
CHANGED
@@ -3,11 +3,13 @@ require 'berater/limiter_set'
|
|
3
3
|
require 'berater/lock'
|
4
4
|
require 'berater/lua_script'
|
5
5
|
require 'berater/middleware'
|
6
|
+
require 'berater/mutex'
|
6
7
|
require 'berater/utils'
|
7
8
|
require 'berater/version'
|
8
9
|
require 'meddleware'
|
9
10
|
|
10
11
|
module Berater
|
12
|
+
include Meddleware
|
11
13
|
extend self
|
12
14
|
|
13
15
|
class Overloaded < StandardError; end
|
@@ -22,12 +24,6 @@ module Berater
|
|
22
24
|
@limiters ||= LimiterSet.new
|
23
25
|
end
|
24
26
|
|
25
|
-
def middleware(&block)
|
26
|
-
(@middleware ||= Meddleware.new).tap do
|
27
|
-
@middleware.instance_eval(&block) if block_given?
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
27
|
def new(key, capacity, **opts)
|
32
28
|
args = []
|
33
29
|
|
@@ -255,19 +255,19 @@ describe Berater::ConcurrencyLimiter do
|
|
255
255
|
let(:limiter) { described_class.new(:key, 10, timeout: 30) }
|
256
256
|
|
257
257
|
it 'works' do
|
258
|
-
expect(limiter.utilization).to
|
258
|
+
expect(limiter.utilization).to eq 0
|
259
259
|
|
260
260
|
2.times { limiter.limit }
|
261
|
-
expect(limiter.utilization).to
|
261
|
+
expect(limiter.utilization).to eq 20
|
262
262
|
|
263
263
|
Timecop.freeze(15)
|
264
264
|
|
265
265
|
8.times { limiter.limit }
|
266
|
-
expect(limiter.utilization).to
|
266
|
+
expect(limiter.utilization).to eq 100
|
267
267
|
|
268
268
|
Timecop.freeze(15)
|
269
269
|
|
270
|
-
expect(limiter.utilization).to
|
270
|
+
expect(limiter.utilization).to eq 80
|
271
271
|
end
|
272
272
|
end
|
273
273
|
|
@@ -65,6 +65,16 @@ describe Berater::Middleware::Trace do
|
|
65
65
|
}.to raise_error(IOError)
|
66
66
|
end
|
67
67
|
end
|
68
|
+
|
69
|
+
context 'when tracer is not defined' do
|
70
|
+
before do
|
71
|
+
allow(instance).to receive(:tracer).and_return(nil)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'still yields' do
|
75
|
+
expect {|b| instance.call(limiter, &b) }.to yield_control
|
76
|
+
end
|
77
|
+
end
|
68
78
|
end
|
69
79
|
|
70
80
|
context 'when used as middleware' do
|
data/spec/mutex_spec.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
describe Berater::Mutex do
|
2
|
+
let(:klass) do
|
3
|
+
Class.new do
|
4
|
+
include Berater::Mutex
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'synchronize' do
|
9
|
+
it { expect(klass).to respond_to(:synchronize) }
|
10
|
+
it { expect(klass.new).to respond_to(:synchronize) }
|
11
|
+
|
12
|
+
it { expect { |block| klass.synchronize(&block) }.to yield_control }
|
13
|
+
|
14
|
+
it 'returns the blocks value' do
|
15
|
+
res = klass.synchronize { 123 }
|
16
|
+
expect(res).to be 123
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'still works and returns nil without a block' do
|
20
|
+
expect(klass.synchronize).to be nil
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'does not allow simultaneous calls' do
|
24
|
+
expect {
|
25
|
+
klass.synchronize do
|
26
|
+
klass.synchronize
|
27
|
+
end
|
28
|
+
}.to be_overloaded
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'allows simultaneous calls with different sub-keys' do
|
32
|
+
expect {
|
33
|
+
klass.synchronize(:a) do
|
34
|
+
klass.synchronize(:b)
|
35
|
+
end
|
36
|
+
}.not_to raise_error
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'allows consecutive calls' do
|
40
|
+
expect {
|
41
|
+
3.times { klass.synchronize }
|
42
|
+
}.not_to raise_error
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'the instance method' do
|
46
|
+
it 'is a pass through to the class method' do
|
47
|
+
expect(klass).to receive(:synchronize)
|
48
|
+
klass.new.synchronize
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'works with arguments' do
|
52
|
+
key = 'key'
|
53
|
+
opts = { timeout: 1 }
|
54
|
+
block = ->{}
|
55
|
+
|
56
|
+
expect(klass).to receive(:synchronize) do |this_key, **these_opts, &this_block|
|
57
|
+
expect(this_key).to be key
|
58
|
+
expect(these_opts).to eq opts
|
59
|
+
expect(this_block).to be block
|
60
|
+
end
|
61
|
+
|
62
|
+
klass.new.synchronize(key, **opts, &block)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe '.mutex_options' do
|
68
|
+
subject { klass.mutex_options }
|
69
|
+
|
70
|
+
it { expect(klass).to respond_to(:mutex_options) }
|
71
|
+
it { is_expected.to be_a Hash }
|
72
|
+
it { is_expected.to be_empty }
|
73
|
+
it { expect(klass.new).not_to respond_to(:mutex_options) }
|
74
|
+
|
75
|
+
context 'when mutex_options are set' do
|
76
|
+
let(:klass) do
|
77
|
+
Class.new do
|
78
|
+
include Berater::Mutex
|
79
|
+
|
80
|
+
mutex_options timeout: 1
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it { is_expected.to eq(timeout: 1) }
|
85
|
+
|
86
|
+
it 'uses mutex_options during synchronize' do
|
87
|
+
expect(Berater::ConcurrencyLimiter).to receive(:new).and_wrap_original do |original, *args, **kwargs|
|
88
|
+
expect(kwargs).to eq(subject)
|
89
|
+
original.call(*args, **kwargs)
|
90
|
+
end
|
91
|
+
|
92
|
+
klass.synchronize
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe 'when extended rather than included' do
|
98
|
+
let(:klass) do
|
99
|
+
Class.new do
|
100
|
+
extend Berater::Mutex
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
it { expect(klass).to respond_to(:synchronize) }
|
105
|
+
it { expect(klass).to respond_to(:mutex_options) }
|
106
|
+
|
107
|
+
it { expect(klass.new).not_to respond_to(:synchronize) }
|
108
|
+
it { expect(klass.new).not_to respond_to(:mutex_options) }
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'when used in a counter' do
|
112
|
+
subject(:counter) { klass.new }
|
113
|
+
|
114
|
+
let(:klass) do
|
115
|
+
class Counter
|
116
|
+
include Berater::Mutex
|
117
|
+
|
118
|
+
@@count = 0
|
119
|
+
@@counts = {}
|
120
|
+
|
121
|
+
def incr
|
122
|
+
synchronize { @@count += 1 }
|
123
|
+
end
|
124
|
+
|
125
|
+
def incr_key(key)
|
126
|
+
synchronize(key) do
|
127
|
+
@@counts[key] ||= 0
|
128
|
+
@@counts[key] += 1
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
Counter
|
133
|
+
end
|
134
|
+
|
135
|
+
it { expect(counter.incr).to eq 1 }
|
136
|
+
it { expect(counter.incr_key(:a)).to eq 1 }
|
137
|
+
|
138
|
+
it 'separates keys' do
|
139
|
+
res = 3.times.map { counter.incr_key(:a) }
|
140
|
+
expect(res).to eq [ 1, 2, 3 ]
|
141
|
+
|
142
|
+
expect(counter.incr_key(:b)).to eq 1
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
data/spec/rate_limiter_spec.rb
CHANGED
@@ -237,17 +237,17 @@ describe Berater::RateLimiter do
|
|
237
237
|
let(:limiter) { described_class.new(:key, 10, :minute) }
|
238
238
|
|
239
239
|
it do
|
240
|
-
expect(limiter.utilization).to
|
240
|
+
expect(limiter.utilization).to eq 0
|
241
241
|
|
242
242
|
2.times { limiter.limit }
|
243
|
-
expect(limiter.utilization).to
|
243
|
+
expect(limiter.utilization).to eq 20
|
244
244
|
|
245
245
|
8.times { limiter.limit }
|
246
|
-
expect(limiter.utilization).to
|
246
|
+
expect(limiter.utilization).to eq 100
|
247
247
|
|
248
248
|
Timecop.freeze(30)
|
249
249
|
|
250
|
-
expect(limiter.utilization).to
|
250
|
+
expect(limiter.utilization).to eq 50
|
251
251
|
end
|
252
252
|
end
|
253
253
|
|
data/spec/static_limiter_spec.rb
CHANGED
@@ -60,13 +60,13 @@ describe Berater::StaticLimiter do
|
|
60
60
|
let(:limiter) { described_class.new(:key, 10) }
|
61
61
|
|
62
62
|
it do
|
63
|
-
expect(limiter.utilization).to
|
63
|
+
expect(limiter.utilization).to eq 0
|
64
64
|
|
65
65
|
2.times { limiter.limit }
|
66
|
-
expect(limiter.utilization).to
|
66
|
+
expect(limiter.utilization).to eq 20
|
67
67
|
|
68
68
|
8.times { limiter.limit }
|
69
|
-
expect(limiter.utilization).to
|
69
|
+
expect(limiter.utilization).to eq 100
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
data/spec/test_mode_spec.rb
CHANGED
@@ -171,4 +171,13 @@ describe Berater::TestMode do
|
|
171
171
|
it_behaves_like 'it supports test_mode'
|
172
172
|
end
|
173
173
|
|
174
|
+
describe 'Mutex' do
|
175
|
+
let(:klass) { Class.new { include Berater::Mutex } }
|
176
|
+
|
177
|
+
context 'when test_mode = :fail' do
|
178
|
+
before { Berater.test_mode = :fail }
|
179
|
+
|
180
|
+
it { expect { klass.synchronize }.to be_overloaded }
|
181
|
+
end
|
182
|
+
end
|
174
183
|
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.
|
4
|
+
version: 0.15.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:
|
11
|
+
date: 2023-10-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: meddleware
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0.
|
19
|
+
version: '0.3'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0.
|
26
|
+
version: '0.3'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: redis
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,34 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: codecov
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
69
|
- !ruby/object:Gem::Dependency
|
84
70
|
name: ddtrace
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
86
72
|
requirements:
|
87
73
|
- - ">="
|
88
74
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
75
|
+
version: '1'
|
90
76
|
type: :development
|
91
77
|
prerelease: false
|
92
78
|
version_requirements: !ruby/object:Gem::Requirement
|
93
79
|
requirements:
|
94
80
|
- - ">="
|
95
81
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
82
|
+
version: '1'
|
97
83
|
- !ruby/object:Gem::Dependency
|
98
84
|
name: dogstatsd-ruby
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,20 +94,6 @@ dependencies:
|
|
108
94
|
- - ">="
|
109
95
|
- !ruby/object:Gem::Version
|
110
96
|
version: '4.3'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: rake
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
97
|
- !ruby/object:Gem::Dependency
|
126
98
|
name: rspec
|
127
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -183,6 +155,7 @@ files:
|
|
183
155
|
- lib/berater/middleware/load_shedder.rb
|
184
156
|
- lib/berater/middleware/statsd.rb
|
185
157
|
- lib/berater/middleware/trace.rb
|
158
|
+
- lib/berater/mutex.rb
|
186
159
|
- lib/berater/rate_limiter.rb
|
187
160
|
- lib/berater/rspec.rb
|
188
161
|
- lib/berater/rspec/matchers.rb
|
@@ -205,6 +178,7 @@ files:
|
|
205
178
|
- spec/middleware/statsd_spec.rb
|
206
179
|
- spec/middleware/trace_spec.rb
|
207
180
|
- spec/middleware_spec.rb
|
181
|
+
- spec/mutex_spec.rb
|
208
182
|
- spec/rate_limiter_spec.rb
|
209
183
|
- spec/riddle_spec.rb
|
210
184
|
- spec/static_limiter_spec.rb
|
@@ -223,35 +197,36 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
223
197
|
requirements:
|
224
198
|
- - ">="
|
225
199
|
- !ruby/object:Gem::Version
|
226
|
-
version: '
|
200
|
+
version: '3'
|
227
201
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
228
202
|
requirements:
|
229
203
|
- - ">="
|
230
204
|
- !ruby/object:Gem::Version
|
231
205
|
version: '0'
|
232
206
|
requirements: []
|
233
|
-
rubygems_version: 3.
|
207
|
+
rubygems_version: 3.3.7
|
234
208
|
signing_key:
|
235
209
|
specification_version: 4
|
236
210
|
summary: Berater
|
237
211
|
test_files:
|
238
|
-
- spec/
|
212
|
+
- spec/berater_spec.rb
|
213
|
+
- spec/concurrency_limiter_spec.rb
|
214
|
+
- spec/dsl_refinement_spec.rb
|
215
|
+
- spec/dsl_spec.rb
|
216
|
+
- spec/inhibitor_spec.rb
|
217
|
+
- spec/limiter_set_spec.rb
|
218
|
+
- spec/limiter_spec.rb
|
219
|
+
- spec/lua_script_spec.rb
|
220
|
+
- spec/matchers_spec.rb
|
221
|
+
- spec/middleware/fail_open_spec.rb
|
239
222
|
- spec/middleware/load_shedder_spec.rb
|
240
223
|
- spec/middleware/statsd_spec.rb
|
241
224
|
- spec/middleware/trace_spec.rb
|
242
|
-
- spec/middleware/fail_open_spec.rb
|
243
|
-
- spec/matchers_spec.rb
|
244
|
-
- spec/dsl_refinement_spec.rb
|
245
|
-
- spec/test_mode_spec.rb
|
246
225
|
- spec/middleware_spec.rb
|
247
|
-
- spec/
|
248
|
-
- spec/
|
249
|
-
- spec/concurrency_limiter_spec.rb
|
226
|
+
- spec/mutex_spec.rb
|
227
|
+
- spec/rate_limiter_spec.rb
|
250
228
|
- spec/riddle_spec.rb
|
251
|
-
- spec/limiter_set_spec.rb
|
252
|
-
- spec/utils_spec.rb
|
253
|
-
- spec/berater_spec.rb
|
254
|
-
- spec/limiter_spec.rb
|
255
229
|
- spec/static_limiter_spec.rb
|
256
|
-
- spec/
|
230
|
+
- spec/test_mode_spec.rb
|
257
231
|
- spec/unlimiter_spec.rb
|
232
|
+
- spec/utils_spec.rb
|