frugal_timeout 0.0.13 → 0.0.14
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/README.md +6 -7
- data/TODO +1 -22
- data/frugal_timeout.gemspec +3 -3
- data/lib/frugal_timeout.rb +71 -185
- data/lib/frugal_timeout/support.rb +151 -0
- data/performance-test +25 -12
- data/spec/frugal_timeout_spec.rb +194 -77
- metadata +6 -5
data/README.md
CHANGED
|
@@ -15,20 +15,20 @@ alternative that uses only 1 thread.
|
|
|
15
15
|
|
|
16
16
|
Also, there's a race condition in the 1.9-2.0 stock timeout. Consider the
|
|
17
17
|
following code:
|
|
18
|
-
```
|
|
18
|
+
```ruby
|
|
19
19
|
timeout(0.02) {
|
|
20
20
|
timeout(0.01, IOError) { sleep }
|
|
21
21
|
}
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
In this case, the stock timeout will most likely
|
|
25
|
-
race condition, sometimes it can also
|
|
24
|
+
In this case, the stock timeout will most likely raise IOError, but, given the
|
|
25
|
+
race condition, sometimes it can also raise Timeout::Error. Just put `sleep 0.1'
|
|
26
26
|
inside stock timeout ensure to trigger that. As of version 0.0.9, frugal_timeout
|
|
27
|
-
will always
|
|
27
|
+
will always raise IOError.
|
|
28
28
|
|
|
29
29
|
## Example
|
|
30
30
|
|
|
31
|
-
```
|
|
31
|
+
```ruby
|
|
32
32
|
require 'frugal_timeout'
|
|
33
33
|
|
|
34
34
|
begin
|
|
@@ -50,8 +50,7 @@ end
|
|
|
50
50
|
|
|
51
51
|
## Installation
|
|
52
52
|
|
|
53
|
-
Tested on Ruby 1.9.3 and 2.0.0
|
|
54
|
-
though).
|
|
53
|
+
Tested on Ruby 1.9.3 and 2.0.0.
|
|
55
54
|
|
|
56
55
|
```
|
|
57
56
|
gem install frugal_timeout
|
data/TODO
CHANGED
|
@@ -1,22 +1 @@
|
|
|
1
|
-
|
|
2
|
-
# * Add docs like in null_object gem.
|
|
3
|
-
|
|
4
|
-
require 'frugal_timeout'
|
|
5
|
-
|
|
6
|
-
FrugalTimeout.dropin!
|
|
7
|
-
|
|
8
|
-
timeout(1.0, Timeout::Error) {
|
|
9
|
-
begin
|
|
10
|
-
sleep 0.8;
|
|
11
|
-
timeout(0.3, Timeout::Error) {
|
|
12
|
-
begin
|
|
13
|
-
sleep 0.2;
|
|
14
|
-
rescue Timeout::Error;
|
|
15
|
-
puts "0.3 expired"
|
|
16
|
-
end
|
|
17
|
-
};
|
|
18
|
-
sleep 86400;
|
|
19
|
-
rescue Timeout::Error;
|
|
20
|
-
puts "1.0 expired"
|
|
21
|
-
end
|
|
22
|
-
}
|
|
1
|
+
* Add more comments.
|
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-
|
|
3
|
+
s.version = '0.0.14'
|
|
4
|
+
s.date = '2014-02-02'
|
|
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']
|
|
@@ -12,5 +12,5 @@ Gem::Specification.new do |s|
|
|
|
12
12
|
s.homepage = 'https://github.com/ledestin/frugal_timeout'
|
|
13
13
|
|
|
14
14
|
s.add_development_dependency 'rspec', '>= 2.13'
|
|
15
|
-
s.add_runtime_dependency '
|
|
15
|
+
s.add_runtime_dependency 'monotonic_time', '~> 0.0'
|
|
16
16
|
end
|
data/lib/frugal_timeout.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Copyright (C) 2013, 2014 by Dmitry Maksyoma <ledestin@gmail.com>
|
|
2
2
|
|
|
3
|
-
require 'hitimes'
|
|
4
3
|
require 'monitor'
|
|
4
|
+
require 'monotonic_time'
|
|
5
5
|
require 'thread'
|
|
6
6
|
require 'timeout'
|
|
7
|
+
require 'frugal_timeout/support'
|
|
7
8
|
|
|
8
9
|
#--
|
|
9
10
|
# {{{1 Rdoc
|
|
@@ -31,28 +32,14 @@ require 'timeout'
|
|
|
31
32
|
#--
|
|
32
33
|
# }}}1
|
|
33
34
|
module FrugalTimeout
|
|
34
|
-
DO_NOTHING = proc {}
|
|
35
|
-
|
|
36
35
|
# {{{1 Error
|
|
37
36
|
class Error < Timeout::Error #:nodoc:
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
# {{{1 MonotonicTime
|
|
41
|
-
class MonotonicTime #:nodoc:
|
|
42
|
-
NANOS_IN_SECOND = 1_000_000_000
|
|
43
|
-
|
|
44
|
-
def self.measure
|
|
45
|
-
start = now
|
|
46
|
-
yield
|
|
47
|
-
now - start
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def self.now
|
|
51
|
-
Hitimes::Interval.now.start_instant.to_f/NANOS_IN_SECOND
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
39
|
# {{{1 Request
|
|
40
|
+
# Timeout request, holding expiry time, what exception to raise and in which
|
|
41
|
+
# thread. It is active by default, but can be defused. If it's defused, then
|
|
42
|
+
# timeout won't be enforced when #enforce is called.
|
|
56
43
|
class Request #:nodoc:
|
|
57
44
|
include Comparable
|
|
58
45
|
@@mutex = Mutex.new
|
|
@@ -68,7 +55,7 @@ module FrugalTimeout
|
|
|
68
55
|
@at <=> other.at
|
|
69
56
|
end
|
|
70
57
|
|
|
71
|
-
# Timeout won't be enforced if you defuse
|
|
58
|
+
# Timeout won't be enforced if you defuse the request.
|
|
72
59
|
def defuse!
|
|
73
60
|
@@mutex.synchronize { @defused = true }
|
|
74
61
|
end
|
|
@@ -77,80 +64,84 @@ module FrugalTimeout
|
|
|
77
64
|
@@mutex.synchronize { @defused }
|
|
78
65
|
end
|
|
79
66
|
|
|
80
|
-
|
|
67
|
+
# Enforce this timeout request, unless it's been defused.
|
|
68
|
+
# Return true if was enforced, false otherwise.
|
|
69
|
+
def enforce
|
|
81
70
|
@@mutex.synchronize {
|
|
82
|
-
return if @defused
|
|
71
|
+
return false if @defused
|
|
83
72
|
|
|
84
73
|
@thread.raise @klass, 'execution expired'
|
|
74
|
+
@defused = true
|
|
85
75
|
true
|
|
86
76
|
}
|
|
87
77
|
end
|
|
88
78
|
end
|
|
89
79
|
|
|
90
80
|
# {{{1 RequestQueue
|
|
81
|
+
# Contains sorted requests to be processed. Calls @onNewNearestRequest when
|
|
82
|
+
# another request becomes the first in line. Calls @onEnforce when expired
|
|
83
|
+
# requests are removed and enforced.
|
|
84
|
+
#
|
|
85
|
+
# #queue adds requests.
|
|
86
|
+
# #enforceExpired removes and enforces requests.
|
|
91
87
|
class RequestQueue #:nodoc:
|
|
88
|
+
include Hookable
|
|
92
89
|
include MonitorMixin
|
|
93
90
|
|
|
94
91
|
def initialize
|
|
95
92
|
super
|
|
96
|
-
|
|
93
|
+
def_hook_synced :onEnforce, :onNewNearestRequest
|
|
97
94
|
@requests, @threadIdx = SortedQueue.new, Storage.new
|
|
98
95
|
|
|
99
|
-
@requests.
|
|
100
|
-
@requests.
|
|
96
|
+
@requests.on_add { |r| @threadIdx.set r.thread, r }
|
|
97
|
+
@requests.on_remove { |r| @threadIdx.delete r.thread, r }
|
|
101
98
|
end
|
|
102
99
|
|
|
103
|
-
def
|
|
100
|
+
def enforceExpired
|
|
104
101
|
synchronize {
|
|
105
|
-
purgeAndEnforceExpired
|
|
106
|
-
sendNearestActiveRequest
|
|
102
|
+
purgeAndEnforceExpired && sendNearestActive
|
|
107
103
|
}
|
|
108
104
|
end
|
|
109
105
|
|
|
110
|
-
def onEnforce &b
|
|
111
|
-
synchronize { @onEnforce = b || DO_NOTHING }
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def onNewNearestRequest &b
|
|
115
|
-
synchronize { @onNewNearestRequest = b || DO_NOTHING }
|
|
116
|
-
end
|
|
117
|
-
|
|
118
106
|
def size
|
|
119
107
|
synchronize { @requests.size }
|
|
120
108
|
end
|
|
121
109
|
|
|
122
110
|
def queue sec, klass
|
|
111
|
+
request = Request.new(Thread.current, MonotonicTime.now + sec, klass)
|
|
123
112
|
synchronize {
|
|
124
|
-
@requests
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
request
|
|
113
|
+
@requests.push(request) {
|
|
114
|
+
@onNewNearestRequest.call request
|
|
115
|
+
}
|
|
128
116
|
}
|
|
117
|
+
request
|
|
129
118
|
end
|
|
130
119
|
|
|
131
120
|
private
|
|
132
121
|
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
# Defuse requests belonging to the passed thread.
|
|
123
|
+
def defuseForThread! thread
|
|
124
|
+
return unless request = @threadIdx[thread]
|
|
135
125
|
|
|
136
|
-
if
|
|
137
|
-
|
|
126
|
+
if request.respond_to? :each
|
|
127
|
+
request.each { |r| r.defuse! }
|
|
138
128
|
else
|
|
139
|
-
|
|
129
|
+
request.defuse!
|
|
140
130
|
end
|
|
141
131
|
end
|
|
142
132
|
|
|
143
|
-
# Purge and enforce expired timeouts.
|
|
144
133
|
def purgeAndEnforceExpired
|
|
145
134
|
@onEnforce.call
|
|
146
135
|
now = MonotonicTime.now
|
|
147
|
-
|
|
148
|
-
r
|
|
149
|
-
|
|
150
|
-
|
|
136
|
+
@requests.reject_until_mismatch! { |r|
|
|
137
|
+
if r.at <= now
|
|
138
|
+
r.enforce && defuseForThread!(r.thread)
|
|
139
|
+
true
|
|
140
|
+
end
|
|
141
|
+
}
|
|
151
142
|
end
|
|
152
143
|
|
|
153
|
-
def
|
|
144
|
+
def sendNearestActive
|
|
154
145
|
@requests.reject_until_mismatch! { |r| r.defused? }
|
|
155
146
|
@onNewNearestRequest.call @requests.first unless @requests.empty?
|
|
156
147
|
end
|
|
@@ -163,25 +154,24 @@ module FrugalTimeout
|
|
|
163
154
|
# 3. After the expiry time comes, execute the callback.
|
|
164
155
|
#
|
|
165
156
|
# It's possible to set a new expiry time before the time set previously
|
|
166
|
-
# expires.
|
|
167
|
-
#
|
|
157
|
+
# expires. However, if the old request has already expired, @onExpiry will
|
|
158
|
+
# still be called.
|
|
168
159
|
class SleeperNotifier #:nodoc:
|
|
160
|
+
include Hookable
|
|
169
161
|
include MonitorMixin
|
|
170
162
|
|
|
171
163
|
def initialize
|
|
172
164
|
super()
|
|
173
|
-
|
|
165
|
+
def_hook_synced :onExpiry
|
|
166
|
+
@condVar, @expireAt = new_cond, nil
|
|
174
167
|
|
|
175
168
|
@thread = Thread.new {
|
|
176
169
|
loop {
|
|
177
170
|
synchronize { @onExpiry }.call if synchronize {
|
|
178
|
-
|
|
179
|
-
unless @expireAt
|
|
180
|
-
wait
|
|
181
|
-
next
|
|
182
|
-
end
|
|
171
|
+
waitForValidRequest
|
|
183
172
|
|
|
184
|
-
timeLeft =
|
|
173
|
+
timeLeft = timeLeftUntilExpiry
|
|
174
|
+
# Prevent processing of the same request again.
|
|
185
175
|
disposeOfRequest
|
|
186
176
|
elapsedTime = MonotonicTime.measure { wait timeLeft }
|
|
187
177
|
|
|
@@ -192,10 +182,6 @@ module FrugalTimeout
|
|
|
192
182
|
ObjectSpace.define_finalizer self, proc { @thread.kill }
|
|
193
183
|
end
|
|
194
184
|
|
|
195
|
-
def onExpiry &b
|
|
196
|
-
synchronize { @onExpiry = b || DO_NOTHING }
|
|
197
|
-
end
|
|
198
|
-
|
|
199
185
|
def expireAt time
|
|
200
186
|
synchronize {
|
|
201
187
|
@expireAt = time
|
|
@@ -205,13 +191,6 @@ module FrugalTimeout
|
|
|
205
191
|
|
|
206
192
|
private
|
|
207
193
|
|
|
208
|
-
def calcTimeLeft
|
|
209
|
-
synchronize {
|
|
210
|
-
delay = @expireAt - MonotonicTime.now
|
|
211
|
-
delay < 0 ? 0 : delay
|
|
212
|
-
}
|
|
213
|
-
end
|
|
214
|
-
|
|
215
194
|
def disposeOfRequest
|
|
216
195
|
@expireAt = nil
|
|
217
196
|
end
|
|
@@ -220,129 +199,26 @@ module FrugalTimeout
|
|
|
220
199
|
@condVar.signal
|
|
221
200
|
end
|
|
222
201
|
|
|
223
|
-
def
|
|
224
|
-
@
|
|
202
|
+
def timeLeftUntilExpiry
|
|
203
|
+
delay = @expireAt - MonotonicTime.now
|
|
204
|
+
delay < 0 ? 0 : delay
|
|
225
205
|
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# {{{1 SortedQueue
|
|
229
|
-
class SortedQueue #:nodoc:
|
|
230
|
-
extend Forwardable
|
|
231
|
-
|
|
232
|
-
def_delegators :@array, :empty?, :first, :size
|
|
233
206
|
|
|
234
|
-
def
|
|
235
|
-
|
|
236
|
-
@array, @unsorted = storage, false
|
|
237
|
-
@onAdd = @onRemove = DO_NOTHING
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def last
|
|
241
|
-
sort!
|
|
242
|
-
@array.last
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def onAdd &b
|
|
246
|
-
@onAdd = b || DO_NOTHING
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def onRemove &b
|
|
250
|
-
@onRemove = b || DO_NOTHING
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def push *args
|
|
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
|
|
261
|
-
}
|
|
262
|
-
@unsorted = true
|
|
263
|
-
args.each { |arg| @onAdd.call arg }
|
|
264
|
-
end
|
|
265
|
-
alias :<< :push
|
|
266
|
-
|
|
267
|
-
def reject! &b
|
|
268
|
-
ar = []
|
|
269
|
-
sort!
|
|
270
|
-
@array.reject! { |el|
|
|
271
|
-
ar << el if b.call el
|
|
272
|
-
}
|
|
273
|
-
ar.each { |el| @onRemove.call el }
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def reject_until_mismatch! &b
|
|
277
|
-
res = []
|
|
278
|
-
reject! { |el|
|
|
279
|
-
break unless b.call el
|
|
280
|
-
|
|
281
|
-
res << el
|
|
282
|
-
}
|
|
283
|
-
res
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
def shift
|
|
287
|
-
sort!
|
|
288
|
-
res = @array.shift
|
|
289
|
-
@onRemove.call res
|
|
290
|
-
res
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
private
|
|
294
|
-
def sort!
|
|
295
|
-
return unless @unsorted
|
|
296
|
-
|
|
297
|
-
@array.sort!
|
|
298
|
-
@unsorted = false
|
|
299
|
-
end
|
|
300
|
-
end
|
|
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]
|
|
207
|
+
def wait sec=nil
|
|
208
|
+
@condVar.wait sec
|
|
322
209
|
end
|
|
323
|
-
alias :[] :get
|
|
324
|
-
|
|
325
|
-
def set key, val
|
|
326
|
-
unless stored = @storage[key]
|
|
327
|
-
@storage[key] = val
|
|
328
|
-
return
|
|
329
|
-
end
|
|
330
210
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
else
|
|
334
|
-
@storage[key] = [stored, val]
|
|
335
|
-
end
|
|
211
|
+
def waitForValidRequest
|
|
212
|
+
wait until @expireAt
|
|
336
213
|
end
|
|
337
214
|
end
|
|
338
215
|
|
|
339
216
|
# {{{1 Main code
|
|
340
|
-
@requestQueue = RequestQueue.new
|
|
341
|
-
sleeper = SleeperNotifier.new
|
|
217
|
+
@requestQueue, sleeper = RequestQueue.new, SleeperNotifier.new
|
|
342
218
|
@requestQueue.onNewNearestRequest { |request|
|
|
343
219
|
sleeper.expireAt request.at
|
|
344
220
|
}
|
|
345
|
-
sleeper.onExpiry { @requestQueue.
|
|
221
|
+
sleeper.onExpiry { @requestQueue.enforceExpired }
|
|
346
222
|
|
|
347
223
|
# {{{2 Methods
|
|
348
224
|
|
|
@@ -367,16 +243,26 @@ module FrugalTimeout
|
|
|
367
243
|
return yield sec if sec.nil? || sec <= 0
|
|
368
244
|
|
|
369
245
|
innerException = klass || Class.new(Timeout::ExitException)
|
|
370
|
-
request = @requestQueue.queue(sec, innerException)
|
|
371
246
|
begin
|
|
372
|
-
|
|
247
|
+
request = @requestQueue.queue(sec, innerException)
|
|
248
|
+
# Defuse is here only for the case when exception comes from the yield
|
|
249
|
+
# block. Otherwise, when timeout exception is raised, the request is
|
|
250
|
+
# defused automatically.
|
|
251
|
+
#
|
|
252
|
+
# Now, if in ensure, timeout exception comes, the request has already been
|
|
253
|
+
# defused automatically, so even if ensure is interrupted, there's no
|
|
254
|
+
# problem.
|
|
255
|
+
begin
|
|
256
|
+
yield sec
|
|
257
|
+
ensure
|
|
258
|
+
request.defuse!
|
|
259
|
+
end
|
|
373
260
|
rescue innerException => e
|
|
374
261
|
raise if klass
|
|
375
262
|
|
|
376
263
|
raise Error, e.message, e.backtrace
|
|
377
264
|
ensure
|
|
378
265
|
@onEnsure.call if @onEnsure
|
|
379
|
-
request.defuse!
|
|
380
266
|
end
|
|
381
267
|
end
|
|
382
268
|
# }}}1
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Copyright (C) 2013, 2014 by Dmitry Maksyoma <ledestin@gmail.com>
|
|
2
|
+
|
|
3
|
+
module FrugalTimeout
|
|
4
|
+
# {{{1 Hookable
|
|
5
|
+
module Hookable
|
|
6
|
+
DO_NOTHING = proc {}
|
|
7
|
+
|
|
8
|
+
def def_hook *names
|
|
9
|
+
names.each { |name|
|
|
10
|
+
eval <<-EOF
|
|
11
|
+
def #{name} &b
|
|
12
|
+
@#{name} = b || DO_NOTHING
|
|
13
|
+
end
|
|
14
|
+
#{name}
|
|
15
|
+
EOF
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def def_hook_synced *names
|
|
20
|
+
names.each { |name|
|
|
21
|
+
eval <<-EOF
|
|
22
|
+
def #{name} &b
|
|
23
|
+
synchronize { @#{name} = b || DO_NOTHING }
|
|
24
|
+
end
|
|
25
|
+
#{name}
|
|
26
|
+
EOF
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# {{{1 SortedQueue
|
|
32
|
+
# Array-like structure, providing automatic sorting of elements. When you're
|
|
33
|
+
# accessing elements via #reject! or #first, the elements you access are
|
|
34
|
+
# sorted. There are some optimizations to ensure that elements aren't sorted
|
|
35
|
+
# each time you call those methods.
|
|
36
|
+
#
|
|
37
|
+
# Provides hooks: on_add, on_remove.
|
|
38
|
+
# To setup, do something like this: `queue.on_add { |el| puts "added #{el}" }'.
|
|
39
|
+
class SortedQueue #:nodoc:
|
|
40
|
+
extend Forwardable
|
|
41
|
+
include Hookable
|
|
42
|
+
|
|
43
|
+
# I don't sort underlying array before calling #first because:
|
|
44
|
+
# 1. When a new element is added, it'll be placed correctly at the beginning
|
|
45
|
+
# if it should be first.
|
|
46
|
+
# 2. If items are removed from the underlying array, it'll be in the sorted
|
|
47
|
+
# state afterwards. Thus, in this case, #first will behave correctly as
|
|
48
|
+
# well.
|
|
49
|
+
def_delegators :@array, :empty?, :first, :size
|
|
50
|
+
|
|
51
|
+
def initialize storage=[]
|
|
52
|
+
super()
|
|
53
|
+
@array, @unsorted = storage, false
|
|
54
|
+
def_hook :on_add, :on_remove
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def push *args
|
|
58
|
+
raise ArgumentError, "block can't be given for multiple elements" \
|
|
59
|
+
if block_given? && args.size > 1
|
|
60
|
+
|
|
61
|
+
args.each { |arg|
|
|
62
|
+
case @array.first <=> arg
|
|
63
|
+
when -1
|
|
64
|
+
@array.push arg
|
|
65
|
+
@unsorted = true
|
|
66
|
+
when 0
|
|
67
|
+
@array.unshift arg
|
|
68
|
+
when 1, nil
|
|
69
|
+
@array.unshift arg
|
|
70
|
+
yield arg if block_given?
|
|
71
|
+
end
|
|
72
|
+
@on_add.call arg
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
alias :<< :push
|
|
76
|
+
|
|
77
|
+
def reject! &b
|
|
78
|
+
sort!
|
|
79
|
+
@array.reject! { |el|
|
|
80
|
+
if b.call el
|
|
81
|
+
@on_remove.call el
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def reject_until_mismatch! &b
|
|
88
|
+
curSize = size
|
|
89
|
+
reject! { |el|
|
|
90
|
+
break unless b.call el
|
|
91
|
+
|
|
92
|
+
true
|
|
93
|
+
}
|
|
94
|
+
curSize == size ? nil : self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
def sort!
|
|
99
|
+
return unless @unsorted
|
|
100
|
+
|
|
101
|
+
@array.sort!
|
|
102
|
+
@unsorted = false
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# {{{1 Storage
|
|
107
|
+
# Stores values for keys, such as:
|
|
108
|
+
# 1. `set key, val' will store val.
|
|
109
|
+
# 2. `set key, val2' will store [val, val2].
|
|
110
|
+
# 3. `delete key, val2' will lead to storing just val again.
|
|
111
|
+
# I.e. array is used only when it's absolutely necessary.
|
|
112
|
+
#
|
|
113
|
+
# While it's harder to write code because of this, we do save memory by not
|
|
114
|
+
# instantiating all those arrays.
|
|
115
|
+
class Storage
|
|
116
|
+
def initialize
|
|
117
|
+
@storage = {}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def delete key, val=nil
|
|
121
|
+
return unless stored = @storage[key]
|
|
122
|
+
|
|
123
|
+
if val.nil? || stored == val
|
|
124
|
+
@storage.delete key
|
|
125
|
+
return
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
stored.delete val
|
|
129
|
+
@storage[key] = stored.first if stored.size == 1
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def get key
|
|
133
|
+
@storage[key]
|
|
134
|
+
end
|
|
135
|
+
alias :[] :get
|
|
136
|
+
|
|
137
|
+
def set key, val
|
|
138
|
+
unless stored = @storage[key]
|
|
139
|
+
@storage[key] = val
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
if stored.is_a? Array
|
|
144
|
+
stored << val
|
|
145
|
+
else
|
|
146
|
+
@storage[key] = [stored, val]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
# }}}1
|
|
151
|
+
end
|
data/performance-test
CHANGED
|
@@ -1,30 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/ruby
|
|
2
2
|
|
|
3
|
+
$LOAD_PATH << './lib'
|
|
3
4
|
require './lib/frugal_timeout'
|
|
4
5
|
|
|
5
6
|
Thread.abort_on_exception = true
|
|
6
7
|
FrugalTimeout.dropin!
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
THREAD_COUNT, TIMES = 150, 100
|
|
10
|
+
|
|
11
|
+
def recursive_timeout n, delay
|
|
9
12
|
start = FrugalTimeout::MonotonicTime.now
|
|
10
|
-
timeout(
|
|
13
|
+
timeout(delay) {
|
|
11
14
|
if n > 1
|
|
12
|
-
recursive_timeout n -= 1
|
|
15
|
+
recursive_timeout n -= 1, delay
|
|
13
16
|
else
|
|
14
17
|
sleep
|
|
15
18
|
end
|
|
16
19
|
}
|
|
17
20
|
rescue FrugalTimeout::Error
|
|
18
|
-
|
|
21
|
+
finish = FrugalTimeout::MonotonicTime.now
|
|
22
|
+
@m.synchronize { @ar << finish - start - delay }
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
@ar, @m = [], Mutex.new
|
|
23
|
-
THREAD_COUNT.times {
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
def run is_random
|
|
26
|
+
@ar, @m = [], Mutex.new
|
|
27
|
+
THREAD_COUNT.times {
|
|
28
|
+
Thread.new {
|
|
29
|
+
delay = 0
|
|
30
|
+
until delay > 0
|
|
31
|
+
delay = is_random ? rand(10) : 1
|
|
32
|
+
end
|
|
33
|
+
recursive_timeout TIMES, delay
|
|
34
|
+
}
|
|
26
35
|
}
|
|
27
|
-
}
|
|
28
|
-
|
|
36
|
+
sleep 0.1 until @m.synchronize { @ar.size == THREAD_COUNT }
|
|
37
|
+
printf "avg over delay %-14s %s\n", is_random ? '(random)' : '(no random)',
|
|
38
|
+
@ar.inject(:+)/@ar.size
|
|
39
|
+
end
|
|
29
40
|
|
|
30
|
-
puts "#{THREAD_COUNT*TIMES} calls
|
|
41
|
+
puts "#{THREAD_COUNT*TIMES} calls"
|
|
42
|
+
run false
|
|
43
|
+
run true
|
data/spec/frugal_timeout_spec.rb
CHANGED
|
@@ -6,11 +6,16 @@ require 'frugal_timeout'
|
|
|
6
6
|
|
|
7
7
|
FrugalTimeout.dropin!
|
|
8
8
|
Thread.abort_on_exception = true
|
|
9
|
-
MonotonicTime = FrugalTimeout::MonotonicTime
|
|
10
9
|
|
|
11
10
|
SMALLEST_TIMEOUT = 0.0000001
|
|
12
11
|
|
|
13
12
|
# {{{1 Helper methods
|
|
13
|
+
class Array
|
|
14
|
+
def avg
|
|
15
|
+
inject(:+)/size
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
def multiple_timeouts growing, cnt
|
|
15
20
|
res, resMutex = [], Mutex.new
|
|
16
21
|
if growing
|
|
@@ -91,14 +96,14 @@ describe FrugalTimeout do
|
|
|
91
96
|
res, resMutex = [], Mutex.new
|
|
92
97
|
(cnt = 5).times { new_timeout_request 1, res, resMutex }
|
|
93
98
|
sleep 1 until res.size == cnt
|
|
94
|
-
res.
|
|
99
|
+
res.avg.round.should == 1
|
|
95
100
|
end
|
|
96
101
|
|
|
97
102
|
it 'handles multiple concurrent same timeouts' do
|
|
98
103
|
res, resMutex = [], Mutex.new
|
|
99
104
|
(cnt = 5).times { new_timeout_request_thread 1, res, resMutex }
|
|
100
105
|
sleep 1 until res.size == cnt
|
|
101
|
-
res.
|
|
106
|
+
(res.avg - 1).should < 0.01
|
|
102
107
|
end
|
|
103
108
|
|
|
104
109
|
context 'recursive timeouts' do
|
|
@@ -205,6 +210,16 @@ describe FrugalTimeout do
|
|
|
205
210
|
sleep 2
|
|
206
211
|
end
|
|
207
212
|
|
|
213
|
+
# Actually, there's a race here, but if timeout exception is raised, it's ok,
|
|
214
|
+
# it just means it was faster than the block exception.
|
|
215
|
+
it "doesn't raise timeout exception when block raises exception" do
|
|
216
|
+
FrugalTimeout.on_ensure { sleep 0.02 }
|
|
217
|
+
expect {
|
|
218
|
+
timeout(0.01) { raise IOError }
|
|
219
|
+
}.to raise_error IOError
|
|
220
|
+
FrugalTimeout.on_ensure
|
|
221
|
+
end
|
|
222
|
+
|
|
208
223
|
it 'handles exception within timeout()' do
|
|
209
224
|
begin
|
|
210
225
|
timeout(1) { raise 'lala' }
|
|
@@ -231,28 +246,60 @@ describe FrugalTimeout do
|
|
|
231
246
|
end
|
|
232
247
|
end
|
|
233
248
|
|
|
234
|
-
# {{{1
|
|
235
|
-
describe FrugalTimeout::
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
249
|
+
# {{{1 Hookable
|
|
250
|
+
describe FrugalTimeout::Hookable do
|
|
251
|
+
before :all do
|
|
252
|
+
class Foo
|
|
253
|
+
include MonitorMixin
|
|
254
|
+
include FrugalTimeout::Hookable
|
|
255
|
+
|
|
256
|
+
def initialize
|
|
257
|
+
super
|
|
258
|
+
def_hook :onBar, :onBar2
|
|
259
|
+
def_hook_synced :onBarSynced, :onBarSynced2
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def run
|
|
263
|
+
@onBar.call
|
|
264
|
+
@onBar2.call
|
|
265
|
+
@onBarSynced.call
|
|
266
|
+
@onBarSynced2.call
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
@foo = Foo.new
|
|
240
271
|
end
|
|
241
272
|
|
|
242
|
-
it '
|
|
243
|
-
|
|
244
|
-
|
|
273
|
+
it 'works w/o user-defined hook' do
|
|
274
|
+
expect { @foo.run }.not_to raise_error
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it 'calls user-defined hook' do
|
|
278
|
+
called = called2 = nil
|
|
279
|
+
@foo.onBar { called = true }
|
|
280
|
+
@foo.onBar2 { called2 = true }
|
|
281
|
+
@foo.run
|
|
282
|
+
called.should == true
|
|
283
|
+
called2.should == true
|
|
245
284
|
end
|
|
246
285
|
end
|
|
247
286
|
|
|
248
287
|
# {{{1 Request
|
|
249
288
|
describe FrugalTimeout::Request do
|
|
250
|
-
|
|
251
|
-
|
|
289
|
+
before :each do
|
|
290
|
+
@request = FrugalTimeout::Request.new(Thread.current,
|
|
252
291
|
MonotonicTime.now, FrugalTimeout::Error)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it '#defuse! and #defused? work' do
|
|
295
|
+
@request.defused?.should == false
|
|
296
|
+
@request.defuse!
|
|
297
|
+
@request.defused?.should == true
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it 'is defused after enforcing' do
|
|
301
|
+
expect { Thread.new { @request.enforce }.join }.to raise_error
|
|
302
|
+
@request.defused?.should == true
|
|
256
303
|
end
|
|
257
304
|
end
|
|
258
305
|
|
|
@@ -287,12 +334,12 @@ describe FrugalTimeout::RequestQueue do
|
|
|
287
334
|
end
|
|
288
335
|
end
|
|
289
336
|
|
|
290
|
-
context 'after
|
|
291
|
-
it 'invokes onEnforce on
|
|
337
|
+
context 'after enforceExpired' do
|
|
338
|
+
it 'invokes onEnforce on enforceExpired' do
|
|
292
339
|
called = false
|
|
293
340
|
@requests.onEnforce { called = true }
|
|
294
341
|
@requests.queue(0, FrugalTimeout::Error)
|
|
295
|
-
expect { @requests.
|
|
342
|
+
expect { @requests.enforceExpired }.to raise_error
|
|
296
343
|
called.should == true
|
|
297
344
|
end
|
|
298
345
|
|
|
@@ -301,7 +348,7 @@ describe FrugalTimeout::RequestQueue do
|
|
|
301
348
|
@requests.queue(0, FrugalTimeout::Error)
|
|
302
349
|
expect {
|
|
303
350
|
Thread.new {
|
|
304
|
-
@requests.
|
|
351
|
+
@requests.enforceExpired
|
|
305
352
|
}.join
|
|
306
353
|
}.to raise_error FrugalTimeout::Error
|
|
307
354
|
req.defused?.should == true
|
|
@@ -310,27 +357,37 @@ describe FrugalTimeout::RequestQueue do
|
|
|
310
357
|
context 'onNewNearestRequest' do
|
|
311
358
|
it 'invokes onNewNearestRequest' do
|
|
312
359
|
@requests.queue(0, FrugalTimeout::Error)
|
|
313
|
-
expect { @requests.
|
|
360
|
+
expect { @requests.enforceExpired }.to raise_error
|
|
314
361
|
@ar.size.should == 1
|
|
315
362
|
end
|
|
316
363
|
|
|
317
364
|
it "doesn't invoke onNewNearestRequest on a defused request" do
|
|
318
365
|
@requests.queue(0, FrugalTimeout::Error).defuse!
|
|
319
|
-
expect { @requests.
|
|
366
|
+
expect { @requests.enforceExpired }.not_to raise_error
|
|
320
367
|
@requests.size.should == 0
|
|
368
|
+
@ar.size == 1
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
it "doesn't invoke onNewNearestRequest if no requests expired yet" do
|
|
372
|
+
@requests.queue(10, FrugalTimeout::Error)
|
|
373
|
+
expect { @requests.enforceExpired }.not_to raise_error
|
|
374
|
+
# 1 has been put there by #queue.
|
|
375
|
+
@ar.size.should == 1
|
|
321
376
|
end
|
|
322
377
|
end
|
|
323
378
|
|
|
324
379
|
it 'no expired requests are left in the queue' do
|
|
325
380
|
@requests.queue(0, FrugalTimeout::Error)
|
|
326
381
|
@requests.size.should == 1
|
|
327
|
-
expect {
|
|
382
|
+
expect {
|
|
383
|
+
Thread.new { @requests.enforceExpired }.join
|
|
384
|
+
}.to raise_error
|
|
328
385
|
@requests.size.should == 0
|
|
329
386
|
end
|
|
330
387
|
|
|
331
388
|
it 'a non-expired request is left in the queue' do
|
|
332
389
|
@requests.queue(10, FrugalTimeout::Error)
|
|
333
|
-
expect { @requests.
|
|
390
|
+
expect { @requests.enforceExpired }.not_to raise_error
|
|
334
391
|
@requests.size.should == 1
|
|
335
392
|
end
|
|
336
393
|
end
|
|
@@ -391,24 +448,99 @@ describe FrugalTimeout::SortedQueue do
|
|
|
391
448
|
@queue = FrugalTimeout::SortedQueue.new
|
|
392
449
|
end
|
|
393
450
|
|
|
394
|
-
|
|
395
|
-
item
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
451
|
+
context '#push' do
|
|
452
|
+
it 'adds item into queue' do
|
|
453
|
+
item = 'a'
|
|
454
|
+
@queue.push item
|
|
455
|
+
@queue.size.should == 1
|
|
456
|
+
@queue.first.should == item
|
|
457
|
+
end
|
|
399
458
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
459
|
+
it 'calls block if element is sorted to be first' do
|
|
460
|
+
called = nil
|
|
461
|
+
@queue.push(2) { |el| called = el }
|
|
462
|
+
called.should == 2
|
|
463
|
+
@queue.push(1) { |el| called = el }
|
|
464
|
+
called.should == 1
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
it "doesn't call block if the pushed element is the same as first" do
|
|
468
|
+
@queue.push 1
|
|
469
|
+
called = nil
|
|
470
|
+
@queue.push(1) { called = true }
|
|
471
|
+
called.should be_nil
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
it "doesn't call block if element isn't sorted to be first" do
|
|
475
|
+
@queue.push 1
|
|
476
|
+
called = nil
|
|
477
|
+
@queue.push(3) { |el| called = el }
|
|
478
|
+
called.should == nil
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
it 'raises exception if block given for multiple pushed elements' do
|
|
482
|
+
expect {
|
|
483
|
+
@queue.push(1, 2) { }
|
|
484
|
+
}.to raise_error ArgumentError
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
context 'sorting' do
|
|
488
|
+
it 'makes first in order item to be sorted first' do
|
|
489
|
+
@queue.push 'b', 'a'
|
|
490
|
+
@queue.first.should == 'a'
|
|
491
|
+
@queue.reject! { |item| item == 'a' }
|
|
492
|
+
@queue.first.should == 'b'
|
|
493
|
+
@queue.size.should == 1
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
context 'works correctly if pushed values are <= the first element' do
|
|
497
|
+
it 'as a single #push call' do
|
|
498
|
+
@queue.push 'c', 'b', 'a'
|
|
499
|
+
ar = []
|
|
500
|
+
@queue.reject! { |el| ar << el }
|
|
501
|
+
ar.should == ['a', 'b', 'c']
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
it 'as multiple push calls' do
|
|
505
|
+
@queue.push 'c'
|
|
506
|
+
@queue.push 'b'
|
|
507
|
+
@queue.push 'a'
|
|
508
|
+
ar = []
|
|
509
|
+
@queue.reject! { |el| ar << el }
|
|
510
|
+
ar.should == ['a', 'b', 'c']
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
it "doesn't sort underlying array if pushed values are first in order" do
|
|
515
|
+
class MockArray < Array
|
|
516
|
+
def sort!
|
|
517
|
+
raise 'not supposed to call sort!'
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
queue = FrugalTimeout::SortedQueue.new MockArray.new
|
|
521
|
+
expect {
|
|
522
|
+
queue.push 'c'
|
|
523
|
+
queue.push 'b'
|
|
524
|
+
queue.push 'a'
|
|
525
|
+
queue.first == 'a'
|
|
526
|
+
queue.reject! { true }
|
|
527
|
+
}.not_to raise_error
|
|
528
|
+
|
|
529
|
+
expect {
|
|
530
|
+
queue.push 'c', 'b', 'a'
|
|
531
|
+
queue.first == 'a'
|
|
532
|
+
queue.reject! {}
|
|
533
|
+
}.not_to raise_error
|
|
534
|
+
end
|
|
535
|
+
end
|
|
403
536
|
end
|
|
404
537
|
|
|
405
|
-
it '
|
|
406
|
-
@queue
|
|
538
|
+
it '#<< method is supported' do
|
|
539
|
+
@queue << 'a'
|
|
407
540
|
@queue.first.should == 'a'
|
|
408
|
-
@queue.last.should == 'b'
|
|
409
541
|
end
|
|
410
542
|
|
|
411
|
-
it '
|
|
543
|
+
it '#reject! works' do
|
|
412
544
|
@queue.push 'a', 'b', 'c'
|
|
413
545
|
@queue.reject! { |item|
|
|
414
546
|
next true if item < 'c'
|
|
@@ -418,51 +550,36 @@ describe FrugalTimeout::SortedQueue do
|
|
|
418
550
|
@queue.first.should == 'c'
|
|
419
551
|
end
|
|
420
552
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
553
|
+
context '#reject_until_mismatch!' do
|
|
554
|
+
it 'removes one of the elements and returns @queue' do
|
|
555
|
+
@queue.push 'a', 'b'
|
|
556
|
+
@queue.reject_until_mismatch! { |el| el < 'b' }.should == @queue
|
|
557
|
+
@queue.size.should == 1
|
|
558
|
+
@queue.first.should == 'b'
|
|
427
559
|
end
|
|
428
|
-
@queue = FrugalTimeout::SortedQueue.new MockArray.new
|
|
429
|
-
expect {
|
|
430
|
-
@queue.push 'c'
|
|
431
|
-
@queue.push 'b'
|
|
432
|
-
@queue.push 'a'
|
|
433
|
-
@queue.first == 'a'
|
|
434
|
-
}.not_to raise_error
|
|
435
|
-
end
|
|
436
560
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
res.first.should == 'a'
|
|
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'
|
|
561
|
+
it "doesn't remove any elements and returns nil" do
|
|
562
|
+
@queue.push 'a', 'b'
|
|
563
|
+
@queue.reject_until_mismatch! { }.should == nil
|
|
564
|
+
@queue.size.should == 2
|
|
565
|
+
end
|
|
451
566
|
end
|
|
452
567
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
568
|
+
context 'callbacks' do
|
|
569
|
+
it 'calls on_add callback' do
|
|
570
|
+
called = nil
|
|
571
|
+
@queue.on_add { |el| called = el }
|
|
572
|
+
@queue.push 'a'
|
|
573
|
+
called.should == 'a'
|
|
574
|
+
end
|
|
459
575
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
576
|
+
it 'calls on_remove callback' do
|
|
577
|
+
called = nil
|
|
578
|
+
@queue.on_remove { |el| called = el }
|
|
579
|
+
@queue.push 'a'
|
|
580
|
+
@queue.reject! { |el| true }
|
|
581
|
+
called.should == 'a'
|
|
582
|
+
end
|
|
466
583
|
end
|
|
467
584
|
end
|
|
468
585
|
|
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.14
|
|
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-
|
|
12
|
+
date: 2014-02-02 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: rspec
|
|
@@ -28,13 +28,13 @@ dependencies:
|
|
|
28
28
|
- !ruby/object:Gem::Version
|
|
29
29
|
version: '2.13'
|
|
30
30
|
- !ruby/object:Gem::Dependency
|
|
31
|
-
name:
|
|
31
|
+
name: monotonic_time
|
|
32
32
|
requirement: !ruby/object:Gem::Requirement
|
|
33
33
|
none: false
|
|
34
34
|
requirements:
|
|
35
35
|
- - ~>
|
|
36
36
|
- !ruby/object:Gem::Version
|
|
37
|
-
version: '
|
|
37
|
+
version: '0.0'
|
|
38
38
|
type: :runtime
|
|
39
39
|
prerelease: false
|
|
40
40
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -42,7 +42,7 @@ dependencies:
|
|
|
42
42
|
requirements:
|
|
43
43
|
- - ~>
|
|
44
44
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: '
|
|
45
|
+
version: '0.0'
|
|
46
46
|
description: Timeout.timeout replacement that uses only 1 thread
|
|
47
47
|
email: ledestin@gmail.com
|
|
48
48
|
executables: []
|
|
@@ -60,6 +60,7 @@ files:
|
|
|
60
60
|
- TODO
|
|
61
61
|
- frugal_timeout.gemspec
|
|
62
62
|
- lib/frugal_timeout.rb
|
|
63
|
+
- lib/frugal_timeout/support.rb
|
|
63
64
|
- performance-test
|
|
64
65
|
- spec/frugal_timeout_spec.rb
|
|
65
66
|
- spec/spec_helper.rb
|