frugal_timeout 0.0.12 → 0.0.13
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.
- 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
|