frugal_timeout 0.0.7 → 0.0.8
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/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: []
|