frugal_timeout 0.0.7 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/frugal_timeout.rb +169 -83
- metadata +19 -3
data/lib/frugal_timeout.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
# Copyright (C) 2013 by Dmitry Maksyoma <ledestin@gmail.com>
|
2
2
|
|
3
3
|
require 'hitimes'
|
4
|
+
require 'monitor'
|
5
|
+
require 'null_object'
|
4
6
|
require 'thread'
|
5
7
|
require 'timeout'
|
6
8
|
|
7
9
|
#--
|
8
10
|
# {{{1 Rdoc
|
9
11
|
#++
|
10
|
-
# Timeout.timeout() replacement using only
|
12
|
+
# Timeout.timeout() replacement using only 1 thread
|
11
13
|
# = Example
|
12
14
|
#
|
13
15
|
# require 'frugal_timeout'
|
@@ -34,7 +36,7 @@ module FrugalTimeout
|
|
34
36
|
class Error < Timeout::Error; end # :nodoc:
|
35
37
|
|
36
38
|
# {{{1 MonotonicTime
|
37
|
-
class MonotonicTime
|
39
|
+
class MonotonicTime # :nodoc:
|
38
40
|
NANOS_IN_SECOND = 1_000_000_000
|
39
41
|
|
40
42
|
def self.measure
|
@@ -47,49 +49,97 @@ module FrugalTimeout
|
|
47
49
|
Hitimes::Interval.now.start_instant.to_f/NANOS_IN_SECOND
|
48
50
|
end
|
49
51
|
end
|
52
|
+
|
50
53
|
# {{{1 Request
|
51
54
|
class Request # :nodoc:
|
52
55
|
include Comparable
|
53
56
|
@@mutex = Mutex.new
|
54
57
|
|
55
|
-
attr_reader :at, :thread
|
58
|
+
attr_reader :at, :exception, :klass, :thread
|
56
59
|
|
57
60
|
def initialize thread, at, klass
|
58
61
|
@thread, @at, @klass = thread, at, klass
|
62
|
+
@defused, @exception = false, Class.new(Timeout::ExitException)
|
59
63
|
end
|
60
64
|
|
61
65
|
def <=>(other)
|
62
66
|
@at <=> other.at
|
63
67
|
end
|
64
68
|
|
65
|
-
|
66
|
-
|
69
|
+
# Timeout won't be enforced if you defuse a request.
|
70
|
+
def defuse!
|
71
|
+
@@mutex.synchronize { @defused = true }
|
67
72
|
end
|
68
73
|
|
69
|
-
def
|
70
|
-
@@mutex.synchronize { @
|
74
|
+
def defused?
|
75
|
+
@@mutex.synchronize { @defused }
|
71
76
|
end
|
72
77
|
|
73
|
-
def enforceTimeout
|
74
|
-
|
78
|
+
def enforceTimeout filter=NullObject.new {}
|
79
|
+
@@mutex.synchronize {
|
80
|
+
return if @defused || filter.has_key?(@thread)
|
81
|
+
|
82
|
+
filter[@thread] = true
|
83
|
+
@thread.raise @exception, 'execution expired'
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# {{{1 RequestQueue
|
89
|
+
class RequestQueue # :nodoc:
|
90
|
+
extend Forwardable
|
91
|
+
|
92
|
+
def_delegators :@requests, :empty?, :first, :<<
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
@onNewNearestRequest, @requests = proc {}, SortedQueue.new
|
96
|
+
end
|
97
|
+
|
98
|
+
def onNewNearestRequest &b
|
99
|
+
@onNewNearestRequest = b
|
100
|
+
end
|
101
|
+
|
102
|
+
# Purge and enforce expired timeouts. Only enforce once for each thread,
|
103
|
+
# even if multiple timeouts for that thread expire at once.
|
104
|
+
def purgeExpired
|
105
|
+
filter, now = {}, MonotonicTime.now
|
106
|
+
@requests.reject_and_get! { |r| r.at <= now }.each { |r|
|
107
|
+
r.enforceTimeout filter
|
108
|
+
}
|
109
|
+
|
110
|
+
@requests.synchronize {
|
111
|
+
@onNewNearestRequest.call(@requests.first) unless @requests.empty?
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def queue sec, klass
|
116
|
+
@requests.synchronize {
|
117
|
+
@requests << (request = Request.new(Thread.current,
|
118
|
+
MonotonicTime.now + sec, klass))
|
119
|
+
@onNewNearestRequest.call(request) if @requests.first == request
|
120
|
+
request
|
121
|
+
}
|
75
122
|
end
|
76
123
|
end
|
77
124
|
|
78
125
|
# {{{1 SleeperNotifier
|
79
126
|
class SleeperNotifier # :nodoc:
|
80
|
-
|
81
|
-
|
82
|
-
|
127
|
+
include MonitorMixin
|
128
|
+
|
129
|
+
def initialize
|
130
|
+
super()
|
131
|
+
@condVar, @onExpiry, @request = new_cond, proc {}, nil
|
83
132
|
|
84
133
|
@thread = Thread.new {
|
85
134
|
loop {
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
135
|
+
@onExpiry.call if synchronize {
|
136
|
+
sleepFor = latestDelay
|
137
|
+
sleptFor = MonotonicTime.measure { @condVar.wait sleepFor }
|
138
|
+
|
139
|
+
if sleepFor && sleptFor >= sleepFor
|
140
|
+
@request = nil
|
141
|
+
true
|
142
|
+
end
|
93
143
|
}
|
94
144
|
}
|
95
145
|
}
|
@@ -98,98 +148,134 @@ module FrugalTimeout
|
|
98
148
|
|
99
149
|
def latestDelay
|
100
150
|
synchronize {
|
101
|
-
|
102
|
-
|
103
|
-
|
151
|
+
return unless @request
|
152
|
+
|
153
|
+
delay = @request.at - MonotonicTime.now
|
154
|
+
delay < 0 ? 0 : delay
|
104
155
|
}
|
105
156
|
end
|
106
157
|
private :latestDelay
|
107
158
|
|
108
|
-
def
|
159
|
+
def notify
|
160
|
+
@condVar.signal
|
161
|
+
end
|
162
|
+
private :notify
|
163
|
+
|
164
|
+
def onExpiry &b
|
165
|
+
@onExpiry = b
|
166
|
+
end
|
167
|
+
|
168
|
+
# 1. Send any request.
|
169
|
+
# 2. Send only nearer (than the first request) expiration times.
|
170
|
+
# 3. The latest passed request expires and @onExpiry is called. Goto 1.
|
171
|
+
def sleepUntilExpires request
|
172
|
+
synchronize {
|
173
|
+
@request = request
|
174
|
+
notify
|
175
|
+
}
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# {{{1 SortedQueue
|
180
|
+
class SortedQueue # :nodoc:
|
181
|
+
include MonitorMixin
|
182
|
+
|
183
|
+
def initialize storage=[]
|
184
|
+
super()
|
185
|
+
@array, @unsorted = storage, false
|
186
|
+
end
|
187
|
+
|
188
|
+
def empty?
|
189
|
+
synchronize { @array.empty? }
|
190
|
+
end
|
191
|
+
|
192
|
+
def first
|
193
|
+
synchronize { @array.first }
|
194
|
+
end
|
195
|
+
|
196
|
+
def last
|
109
197
|
synchronize {
|
110
|
-
|
111
|
-
@
|
112
|
-
@thread.wakeup
|
198
|
+
sort!
|
199
|
+
@array.last
|
113
200
|
}
|
114
201
|
end
|
115
202
|
|
116
|
-
def
|
117
|
-
|
203
|
+
def push *args
|
204
|
+
synchronize {
|
205
|
+
args.each { |arg|
|
206
|
+
case @array.first <=> arg
|
207
|
+
when -1, 0, nil
|
208
|
+
@array.push arg
|
209
|
+
when 1
|
210
|
+
@array.unshift arg
|
211
|
+
end
|
212
|
+
}
|
213
|
+
@unsorted = true
|
214
|
+
}
|
215
|
+
end
|
216
|
+
alias :<< :push
|
217
|
+
|
218
|
+
def reject! &b
|
219
|
+
synchronize {
|
220
|
+
sort!
|
221
|
+
@array.reject! &b
|
222
|
+
}
|
223
|
+
end
|
224
|
+
|
225
|
+
def reject_and_get! &b
|
226
|
+
res = []
|
227
|
+
reject! { |el|
|
228
|
+
break unless b.call el
|
229
|
+
|
230
|
+
res << el
|
231
|
+
}
|
232
|
+
res
|
233
|
+
end
|
234
|
+
|
235
|
+
def size
|
236
|
+
synchronize { @array.size }
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
def sort!
|
241
|
+
return unless @unsorted
|
242
|
+
|
243
|
+
@array.sort!
|
244
|
+
@unsorted = false
|
118
245
|
end
|
119
|
-
private :synchronize
|
120
246
|
end
|
121
247
|
|
122
248
|
# {{{1 Main code
|
123
|
-
@
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
Thread.new {
|
128
|
-
nearestTimeout, requests = nil, []
|
129
|
-
loop {
|
130
|
-
request = @in.shift
|
131
|
-
now = MonotonicTime.now
|
132
|
-
|
133
|
-
if request == :expired
|
134
|
-
# Enforce all expired timeouts.
|
135
|
-
requests.sort!
|
136
|
-
requests.each_with_index { |r, i|
|
137
|
-
break if r.at > now
|
138
|
-
|
139
|
-
r.enforceTimeout
|
140
|
-
requests[i] = nil
|
141
|
-
}
|
142
|
-
requests.compact!
|
143
|
-
|
144
|
-
# Activate the nearest non-expired timeout.
|
145
|
-
nearestTimeout = unless requests.first
|
146
|
-
nil
|
147
|
-
else
|
148
|
-
@sleeper.notifyAfter requests.first.at - now
|
149
|
-
requests.first.at
|
150
|
-
end
|
151
|
-
|
152
|
-
next
|
153
|
-
end
|
154
|
-
|
155
|
-
# New timeout request.
|
156
|
-
# Already expired, enforce right away.
|
157
|
-
if request.at <= now
|
158
|
-
request.enforceTimeout
|
159
|
-
next
|
160
|
-
end
|
161
|
-
|
162
|
-
# Queue new timeout for later enforcing. Activate if it's nearest to
|
163
|
-
# enforce.
|
164
|
-
requests << request
|
165
|
-
next if nearestTimeout && request.at > nearestTimeout
|
166
|
-
|
167
|
-
@sleeper.notifyAfter request.at - now
|
168
|
-
nearestTimeout = request.at
|
169
|
-
}
|
249
|
+
@requestQueue = RequestQueue.new
|
250
|
+
sleeper = SleeperNotifier.new
|
251
|
+
@requestQueue.onNewNearestRequest { |request|
|
252
|
+
sleeper.sleepUntilExpires request
|
170
253
|
}
|
171
|
-
|
254
|
+
sleeper.onExpiry { @requestQueue.purgeExpired }
|
172
255
|
|
173
256
|
# {{{2 Methods
|
174
257
|
|
175
258
|
# Ensure that calling timeout() will use FrugalTimeout.timeout()
|
176
259
|
def self.dropin!
|
177
260
|
Object.class_eval \
|
178
|
-
'def timeout t, klass=
|
261
|
+
'def timeout t, klass=Error, &b
|
179
262
|
FrugalTimeout.timeout t, klass, &b
|
180
263
|
end'
|
181
264
|
end
|
182
265
|
|
183
266
|
# Same as Timeout.timeout()
|
184
|
-
def self.timeout sec, klass=
|
185
|
-
return yield sec if sec
|
267
|
+
def self.timeout sec, klass=Error
|
268
|
+
return yield sec if sec.nil? || sec <= 0
|
186
269
|
|
187
|
-
|
188
|
-
klass)
|
270
|
+
request = @requestQueue.queue(sec, klass)
|
189
271
|
begin
|
190
272
|
yield sec
|
273
|
+
rescue request.exception => e
|
274
|
+
raise unless e.is_a? Timeout::ExitException
|
275
|
+
|
276
|
+
raise request.klass, e.message, e.backtrace
|
191
277
|
ensure
|
192
|
-
request.
|
278
|
+
request.defuse!
|
193
279
|
end
|
194
280
|
end
|
195
281
|
# }}}1
|
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.8
|
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: 2013-
|
12
|
+
date: 2013-12-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -43,7 +43,23 @@ dependencies:
|
|
43
43
|
- - ~>
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '1.2'
|
46
|
-
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: null_object
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.0'
|
62
|
+
description: Timeout.timeout replacement that uses only 1 thread
|
47
63
|
email: ledestin@gmail.com
|
48
64
|
executables: []
|
49
65
|
extensions: []
|