frugal_timeout 0.0.12 → 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- data/TODO +1 -0
- data/frugal_timeout.gemspec +2 -2
- data/lib/frugal_timeout.rb +118 -83
- data/performance-test +30 -0
- data/spec/frugal_timeout_spec.rb +179 -13
- metadata +3 -2
data/TODO
CHANGED
data/frugal_timeout.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'frugal_timeout'
|
3
|
-
s.version = '0.0.
|
4
|
-
s.date = '2014-01-
|
3
|
+
s.version = '0.0.13'
|
4
|
+
s.date = '2014-01-08'
|
5
5
|
s.summary = 'Timeout.timeout replacement'
|
6
6
|
s.description = 'Timeout.timeout replacement that uses only 1 thread'
|
7
7
|
s.authors = ['Dmitry Maksyoma']
|
data/lib/frugal_timeout.rb
CHANGED
@@ -31,6 +31,8 @@ require 'timeout'
|
|
31
31
|
#--
|
32
32
|
# }}}1
|
33
33
|
module FrugalTimeout
|
34
|
+
DO_NOTHING = proc {}
|
35
|
+
|
34
36
|
# {{{1 Error
|
35
37
|
class Error < Timeout::Error #:nodoc:
|
36
38
|
end
|
@@ -87,75 +89,70 @@ module FrugalTimeout
|
|
87
89
|
|
88
90
|
# {{{1 RequestQueue
|
89
91
|
class RequestQueue #:nodoc:
|
90
|
-
|
91
|
-
|
92
|
-
def_delegators :@requests, :empty?, :first, :<<
|
92
|
+
include MonitorMixin
|
93
93
|
|
94
94
|
def initialize
|
95
|
-
|
96
|
-
|
95
|
+
super
|
96
|
+
@onEnforce, @onNewNearestRequest = DO_NOTHING, DO_NOTHING
|
97
|
+
@requests, @threadIdx = SortedQueue.new, Storage.new
|
98
|
+
|
99
|
+
@requests.onAdd { |r| @threadIdx.set r.thread, r }
|
100
|
+
@requests.onRemove { |r| @threadIdx.delete r.thread, r }
|
97
101
|
end
|
98
102
|
|
99
|
-
def
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
+
def handleExpiry
|
104
|
+
synchronize {
|
105
|
+
purgeAndEnforceExpired
|
106
|
+
sendNearestActiveRequest
|
103
107
|
}
|
104
108
|
end
|
105
|
-
private :defuse_thread!
|
106
109
|
|
107
110
|
def onEnforce &b
|
108
|
-
@onEnforce = b
|
111
|
+
synchronize { @onEnforce = b || DO_NOTHING }
|
109
112
|
end
|
110
113
|
|
111
114
|
def onNewNearestRequest &b
|
112
|
-
@onNewNearestRequest = b
|
115
|
+
synchronize { @onNewNearestRequest = b || DO_NOTHING }
|
113
116
|
end
|
114
117
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
filter, now = {}, MonotonicTime.now
|
119
|
-
@requests.synchronize {
|
120
|
-
@onEnforce.call if @onEnforce
|
121
|
-
|
122
|
-
@requests.reject_and_get! { |r| r.at <= now }.each { |r|
|
123
|
-
next if filter[r.thread]
|
124
|
-
|
125
|
-
if r.enforceTimeout
|
126
|
-
defuse_thread! r.thread
|
127
|
-
filter[r.thread] = true
|
128
|
-
end
|
129
|
-
}
|
118
|
+
def size
|
119
|
+
synchronize { @requests.size }
|
120
|
+
end
|
130
121
|
|
131
|
-
|
132
|
-
|
133
|
-
@
|
122
|
+
def queue sec, klass
|
123
|
+
synchronize {
|
124
|
+
@requests << (request = Request.new(Thread.current,
|
125
|
+
MonotonicTime.now + sec, klass))
|
126
|
+
@onNewNearestRequest.call(request) if @requests.first == request
|
127
|
+
request
|
134
128
|
}
|
135
129
|
end
|
136
130
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
end
|
131
|
+
private
|
132
|
+
|
133
|
+
def defuse_thread! thread
|
134
|
+
return unless stored = @threadIdx[thread]
|
142
135
|
|
143
136
|
if stored.is_a? Array
|
144
|
-
stored
|
137
|
+
stored.each { |r| r.defuse! }
|
145
138
|
else
|
146
|
-
|
139
|
+
stored.defuse!
|
147
140
|
end
|
148
141
|
end
|
149
|
-
private :storeInIndex
|
150
142
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
143
|
+
# Purge and enforce expired timeouts.
|
144
|
+
def purgeAndEnforceExpired
|
145
|
+
@onEnforce.call
|
146
|
+
now = MonotonicTime.now
|
147
|
+
while !@requests.empty? && @requests.first.at <= now
|
148
|
+
r = @requests.shift
|
149
|
+
r.enforceTimeout && defuse_thread!(r.thread)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def sendNearestActiveRequest
|
154
|
+
@requests.reject_until_mismatch! { |r| r.defused? }
|
155
|
+
@onNewNearestRequest.call @requests.first unless @requests.empty?
|
159
156
|
end
|
160
157
|
end
|
161
158
|
|
@@ -171,8 +168,6 @@ module FrugalTimeout
|
|
171
168
|
class SleeperNotifier #:nodoc:
|
172
169
|
include MonitorMixin
|
173
170
|
|
174
|
-
DO_NOTHING = proc {}
|
175
|
-
|
176
171
|
def initialize
|
177
172
|
super()
|
178
173
|
@condVar, @expireAt, @onExpiry = new_cond, nil, DO_NOTHING
|
@@ -232,55 +227,53 @@ module FrugalTimeout
|
|
232
227
|
|
233
228
|
# {{{1 SortedQueue
|
234
229
|
class SortedQueue #:nodoc:
|
235
|
-
|
230
|
+
extend Forwardable
|
231
|
+
|
232
|
+
def_delegators :@array, :empty?, :first, :size
|
236
233
|
|
237
234
|
def initialize storage=[]
|
238
235
|
super()
|
239
236
|
@array, @unsorted = storage, false
|
237
|
+
@onAdd = @onRemove = DO_NOTHING
|
240
238
|
end
|
241
239
|
|
242
|
-
def
|
243
|
-
|
240
|
+
def last
|
241
|
+
sort!
|
242
|
+
@array.last
|
244
243
|
end
|
245
244
|
|
246
|
-
def
|
247
|
-
|
245
|
+
def onAdd &b
|
246
|
+
@onAdd = b || DO_NOTHING
|
248
247
|
end
|
249
248
|
|
250
|
-
def
|
251
|
-
|
252
|
-
end
|
253
|
-
|
254
|
-
def last
|
255
|
-
synchronize {
|
256
|
-
sort!
|
257
|
-
@array.last
|
258
|
-
}
|
249
|
+
def onRemove &b
|
250
|
+
@onRemove = b || DO_NOTHING
|
259
251
|
end
|
260
252
|
|
261
253
|
def push *args
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
end
|
270
|
-
}
|
271
|
-
@unsorted = true
|
254
|
+
args.each { |arg|
|
255
|
+
case @array.first <=> arg
|
256
|
+
when -1, 0, nil
|
257
|
+
@array.push arg
|
258
|
+
when 1
|
259
|
+
@array.unshift arg
|
260
|
+
end
|
272
261
|
}
|
262
|
+
@unsorted = true
|
263
|
+
args.each { |arg| @onAdd.call arg }
|
273
264
|
end
|
274
265
|
alias :<< :push
|
275
266
|
|
276
267
|
def reject! &b
|
277
|
-
|
278
|
-
|
279
|
-
|
268
|
+
ar = []
|
269
|
+
sort!
|
270
|
+
@array.reject! { |el|
|
271
|
+
ar << el if b.call el
|
280
272
|
}
|
273
|
+
ar.each { |el| @onRemove.call el }
|
281
274
|
end
|
282
275
|
|
283
|
-
def
|
276
|
+
def reject_until_mismatch! &b
|
284
277
|
res = []
|
285
278
|
reject! { |el|
|
286
279
|
break unless b.call el
|
@@ -290,8 +283,11 @@ module FrugalTimeout
|
|
290
283
|
res
|
291
284
|
end
|
292
285
|
|
293
|
-
def
|
294
|
-
|
286
|
+
def shift
|
287
|
+
sort!
|
288
|
+
res = @array.shift
|
289
|
+
@onRemove.call res
|
290
|
+
res
|
295
291
|
end
|
296
292
|
|
297
293
|
private
|
@@ -303,20 +299,57 @@ module FrugalTimeout
|
|
303
299
|
end
|
304
300
|
end
|
305
301
|
|
302
|
+
# {{{1 Storage
|
303
|
+
class Storage
|
304
|
+
def initialize
|
305
|
+
@storage = {}
|
306
|
+
end
|
307
|
+
|
308
|
+
def delete key, val=nil
|
309
|
+
return unless stored = @storage[key]
|
310
|
+
|
311
|
+
if val.nil? || stored == val
|
312
|
+
@storage.delete key
|
313
|
+
return
|
314
|
+
end
|
315
|
+
|
316
|
+
stored.delete val
|
317
|
+
@storage[key] = stored.first if stored.size == 1
|
318
|
+
end
|
319
|
+
|
320
|
+
def get key
|
321
|
+
@storage[key]
|
322
|
+
end
|
323
|
+
alias :[] :get
|
324
|
+
|
325
|
+
def set key, val
|
326
|
+
unless stored = @storage[key]
|
327
|
+
@storage[key] = val
|
328
|
+
return
|
329
|
+
end
|
330
|
+
|
331
|
+
if stored.is_a? Array
|
332
|
+
stored << val
|
333
|
+
else
|
334
|
+
@storage[key] = [stored, val]
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
306
339
|
# {{{1 Main code
|
307
340
|
@requestQueue = RequestQueue.new
|
308
341
|
sleeper = SleeperNotifier.new
|
309
342
|
@requestQueue.onNewNearestRequest { |request|
|
310
343
|
sleeper.expireAt request.at
|
311
344
|
}
|
312
|
-
sleeper.onExpiry { @requestQueue.
|
345
|
+
sleeper.onExpiry { @requestQueue.handleExpiry }
|
313
346
|
|
314
347
|
# {{{2 Methods
|
315
348
|
|
316
349
|
# Ensure that calling timeout() will use FrugalTimeout.timeout()
|
317
350
|
def self.dropin!
|
318
351
|
Object.class_eval \
|
319
|
-
'def timeout t, klass=
|
352
|
+
'def timeout t, klass=nil, &b
|
320
353
|
FrugalTimeout.timeout t, klass, &b
|
321
354
|
end'
|
322
355
|
end
|
@@ -330,15 +363,17 @@ module FrugalTimeout
|
|
330
363
|
end
|
331
364
|
|
332
365
|
# Same as Timeout.timeout()
|
333
|
-
def self.timeout sec, klass=
|
366
|
+
def self.timeout sec, klass=nil
|
334
367
|
return yield sec if sec.nil? || sec <= 0
|
335
368
|
|
336
|
-
innerException = Class.new
|
369
|
+
innerException = klass || Class.new(Timeout::ExitException)
|
337
370
|
request = @requestQueue.queue(sec, innerException)
|
338
371
|
begin
|
339
372
|
yield sec
|
340
373
|
rescue innerException => e
|
341
|
-
raise klass
|
374
|
+
raise if klass
|
375
|
+
|
376
|
+
raise Error, e.message, e.backtrace
|
342
377
|
ensure
|
343
378
|
@onEnsure.call if @onEnsure
|
344
379
|
request.defuse!
|
data/performance-test
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require './lib/frugal_timeout'
|
4
|
+
|
5
|
+
Thread.abort_on_exception = true
|
6
|
+
FrugalTimeout.dropin!
|
7
|
+
|
8
|
+
def recursive_timeout n
|
9
|
+
start = FrugalTimeout::MonotonicTime.now
|
10
|
+
timeout(1) {
|
11
|
+
if n > 1
|
12
|
+
recursive_timeout n -= 1
|
13
|
+
else
|
14
|
+
sleep
|
15
|
+
end
|
16
|
+
}
|
17
|
+
rescue FrugalTimeout::Error
|
18
|
+
@m.synchronize { @ar << FrugalTimeout::MonotonicTime.now - start }
|
19
|
+
end
|
20
|
+
|
21
|
+
THREAD_COUNT, TIMES = 150, 100
|
22
|
+
@ar, @m = [], Mutex.new
|
23
|
+
THREAD_COUNT.times {
|
24
|
+
Thread.new {
|
25
|
+
recursive_timeout TIMES
|
26
|
+
}
|
27
|
+
}
|
28
|
+
sleep 0.1 until @m.synchronize { @ar.size == THREAD_COUNT }
|
29
|
+
|
30
|
+
puts "#{THREAD_COUNT*TIMES} calls\navg: #{@ar.inject(:+)/@ar.size}"
|
data/spec/frugal_timeout_spec.rb
CHANGED
@@ -23,12 +23,10 @@ def multiple_timeouts growing, cnt
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def new_timeout_request sec, res, resMutex
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
resMutex.synchronize { res << MonotonicTime.now - start }
|
31
|
-
end
|
26
|
+
start = MonotonicTime.now
|
27
|
+
timeout(sec) { sleep }
|
28
|
+
rescue FrugalTimeout::Error
|
29
|
+
resMutex.synchronize { res << MonotonicTime.now - start }
|
32
30
|
end
|
33
31
|
|
34
32
|
def new_timeout_request_thread sec, res, resMutex
|
@@ -104,6 +102,14 @@ describe FrugalTimeout do
|
|
104
102
|
end
|
105
103
|
|
106
104
|
context 'recursive timeouts' do
|
105
|
+
it 'with the same delay' do
|
106
|
+
expect {
|
107
|
+
timeout(SMALLEST_TIMEOUT) {
|
108
|
+
timeout(SMALLEST_TIMEOUT) { sleep }
|
109
|
+
}
|
110
|
+
}.to raise_error FrugalTimeout::Error
|
111
|
+
end
|
112
|
+
|
107
113
|
it 'works if recursive timeouts rescue thrown exception' do
|
108
114
|
# A rescue block will only catch exception for the timeout() block it's
|
109
115
|
# written for.
|
@@ -183,6 +189,17 @@ describe FrugalTimeout do
|
|
183
189
|
expect { timeout(0.1, IOError) { sleep } }.to raise_error IOError
|
184
190
|
end
|
185
191
|
|
192
|
+
it 'raises specified exception inside the block' do
|
193
|
+
expect {
|
194
|
+
timeout(0.01, IOError) {
|
195
|
+
begin
|
196
|
+
sleep
|
197
|
+
rescue IOError
|
198
|
+
end
|
199
|
+
}
|
200
|
+
}.not_to raise_error
|
201
|
+
end
|
202
|
+
|
186
203
|
it "doesn't raise exception if there's no need" do
|
187
204
|
timeout(1) { }
|
188
205
|
sleep 2
|
@@ -205,6 +222,13 @@ describe FrugalTimeout do
|
|
205
222
|
expect { timeout(SMALLEST_TIMEOUT) { sleep } }.to \
|
206
223
|
raise_error Timeout::Error
|
207
224
|
end
|
225
|
+
|
226
|
+
it "doesn't enforce defused timeout" do
|
227
|
+
expect {
|
228
|
+
timeout(0.1) { }
|
229
|
+
sleep 0.2
|
230
|
+
}.not_to raise_error
|
231
|
+
end
|
208
232
|
end
|
209
233
|
|
210
234
|
# {{{1 MonotonicTime
|
@@ -242,13 +266,73 @@ describe FrugalTimeout::RequestQueue do
|
|
242
266
|
}
|
243
267
|
end
|
244
268
|
|
245
|
-
context '
|
246
|
-
|
247
|
-
it
|
248
|
-
|
269
|
+
context 'after queueing' do
|
270
|
+
context 'invokes onNewNearestRequest callback' do
|
271
|
+
it 'just once' do
|
272
|
+
@requests.queue(10, FrugalTimeout::Error)
|
249
273
|
@ar.size.should == 1
|
250
274
|
end
|
251
|
-
|
275
|
+
|
276
|
+
it 'when next request is nearer than prev' do
|
277
|
+
@requests.queue(10, FrugalTimeout::Error)
|
278
|
+
@requests.queue(0, FrugalTimeout::Error)
|
279
|
+
@ar.size.should == 2
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
it "doesn't invoke onNewNearestRequest if request isn't nearest" do
|
284
|
+
@requests.queue(10, FrugalTimeout::Error)
|
285
|
+
@requests.queue(20, FrugalTimeout::Error)
|
286
|
+
@ar.size.should == 1
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
context 'after handleExpiry' do
|
291
|
+
it 'invokes onEnforce on handleExpiry' do
|
292
|
+
called = false
|
293
|
+
@requests.onEnforce { called = true }
|
294
|
+
@requests.queue(0, FrugalTimeout::Error)
|
295
|
+
expect { @requests.handleExpiry }.to raise_error
|
296
|
+
called.should == true
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'defuses all requests for the thread' do
|
300
|
+
req = @requests.queue(10, FrugalTimeout::Error)
|
301
|
+
@requests.queue(0, FrugalTimeout::Error)
|
302
|
+
expect {
|
303
|
+
Thread.new {
|
304
|
+
@requests.handleExpiry
|
305
|
+
}.join
|
306
|
+
}.to raise_error FrugalTimeout::Error
|
307
|
+
req.defused?.should == true
|
308
|
+
end
|
309
|
+
|
310
|
+
context 'onNewNearestRequest' do
|
311
|
+
it 'invokes onNewNearestRequest' do
|
312
|
+
@requests.queue(0, FrugalTimeout::Error)
|
313
|
+
expect { @requests.handleExpiry }.to raise_error
|
314
|
+
@ar.size.should == 1
|
315
|
+
end
|
316
|
+
|
317
|
+
it "doesn't invoke onNewNearestRequest on a defused request" do
|
318
|
+
@requests.queue(0, FrugalTimeout::Error).defuse!
|
319
|
+
expect { @requests.handleExpiry }.not_to raise_error
|
320
|
+
@requests.size.should == 0
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'no expired requests are left in the queue' do
|
325
|
+
@requests.queue(0, FrugalTimeout::Error)
|
326
|
+
@requests.size.should == 1
|
327
|
+
expect { @requests.handleExpiry }.to raise_error
|
328
|
+
@requests.size.should == 0
|
329
|
+
end
|
330
|
+
|
331
|
+
it 'a non-expired request is left in the queue' do
|
332
|
+
@requests.queue(10, FrugalTimeout::Error)
|
333
|
+
expect { @requests.handleExpiry }.not_to raise_error
|
334
|
+
@requests.size.should == 1
|
335
|
+
end
|
252
336
|
end
|
253
337
|
end
|
254
338
|
|
@@ -350,11 +434,93 @@ describe FrugalTimeout::SortedQueue do
|
|
350
434
|
}.not_to raise_error
|
351
435
|
end
|
352
436
|
|
353
|
-
it '#
|
437
|
+
it '#reject_until_mismatch!' do
|
354
438
|
@queue.push 'a'
|
355
439
|
@queue.push 'b'
|
356
|
-
res = @queue.
|
440
|
+
res = @queue.reject_until_mismatch! { |el| el < 'b' }
|
357
441
|
res.size.should == 1
|
358
442
|
res.first.should == 'a'
|
359
443
|
end
|
444
|
+
|
445
|
+
it '#shift calls onRemove' do
|
446
|
+
called = nil
|
447
|
+
@queue.onRemove { |el| called = el }
|
448
|
+
@queue.push 'a'
|
449
|
+
@queue.shift
|
450
|
+
called.should == 'a'
|
451
|
+
end
|
452
|
+
|
453
|
+
it 'calls onAdd callback' do
|
454
|
+
called = nil
|
455
|
+
@queue.onAdd { |el| called = el }
|
456
|
+
@queue.push 'a'
|
457
|
+
called.should == 'a'
|
458
|
+
end
|
459
|
+
|
460
|
+
it 'calls onRemove callback' do
|
461
|
+
called = nil
|
462
|
+
@queue.onRemove { |el| called = el }
|
463
|
+
@queue.push 'a'
|
464
|
+
@queue.reject! { |el| true }
|
465
|
+
called.should == 'a'
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
# {{{1 Storage
|
470
|
+
describe FrugalTimeout::Storage do
|
471
|
+
before :each do
|
472
|
+
@storage = FrugalTimeout::Storage.new
|
473
|
+
end
|
474
|
+
|
475
|
+
context 'for a single key' do
|
476
|
+
it 'contains nothing at first' do
|
477
|
+
@storage.get(1).should == nil
|
478
|
+
end
|
479
|
+
|
480
|
+
it 'stores single value as non-array' do
|
481
|
+
@storage.set 1, 2
|
482
|
+
@storage.get(1).should == 2
|
483
|
+
end
|
484
|
+
|
485
|
+
it 'stores 2 values as array' do
|
486
|
+
@storage.set 1, 2
|
487
|
+
@storage.set 1, 3
|
488
|
+
@storage.get(1).should == [2, 3]
|
489
|
+
end
|
490
|
+
|
491
|
+
context 'removes single value' do
|
492
|
+
it 'and nothing is left' do
|
493
|
+
@storage.set 1, 2
|
494
|
+
@storage.delete 1, 2
|
495
|
+
@storage.get(1).should == nil
|
496
|
+
end
|
497
|
+
|
498
|
+
it 'and single value is left' do
|
499
|
+
@storage.set 1, 2
|
500
|
+
@storage.set 1, 3
|
501
|
+
@storage.delete 1, 2
|
502
|
+
@storage.get(1).should == 3
|
503
|
+
end
|
504
|
+
|
505
|
+
it 'and several values left' do
|
506
|
+
@storage.set 1, 2
|
507
|
+
@storage.set 1, 3
|
508
|
+
@storage.set 1, 4
|
509
|
+
@storage.delete 1, 2
|
510
|
+
@storage.get(1).should == [3, 4]
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
it 'removes everything if no value is given' do
|
515
|
+
@storage.set 1, 2
|
516
|
+
@storage.set 1, 3
|
517
|
+
@storage.delete 1
|
518
|
+
@storage.get(1).should == nil
|
519
|
+
end
|
520
|
+
|
521
|
+
it 'supports #[] as #get' do
|
522
|
+
@storage.set 1, 2
|
523
|
+
@storage[1].should == 2
|
524
|
+
end
|
525
|
+
end
|
360
526
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: frugal_timeout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.13
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-01-
|
12
|
+
date: 2014-01-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -60,6 +60,7 @@ files:
|
|
60
60
|
- TODO
|
61
61
|
- frugal_timeout.gemspec
|
62
62
|
- lib/frugal_timeout.rb
|
63
|
+
- performance-test
|
63
64
|
- spec/frugal_timeout_spec.rb
|
64
65
|
- spec/spec_helper.rb
|
65
66
|
homepage: https://github.com/ledestin/frugal_timeout
|