frugal_timeout 0.0.13 → 0.0.14
Sign up to get free protection for your applications and to get access to all the features.
- 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
|