frugal_timeout 0.0.8 → 0.0.9
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 +77 -45
- metadata +2 -18
data/lib/frugal_timeout.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require 'hitimes'
|
4
4
|
require 'monitor'
|
5
|
-
require 'null_object'
|
6
5
|
require 'thread'
|
7
6
|
require 'timeout'
|
8
7
|
|
@@ -55,11 +54,11 @@ module FrugalTimeout
|
|
55
54
|
include Comparable
|
56
55
|
@@mutex = Mutex.new
|
57
56
|
|
58
|
-
attr_reader :at, :
|
57
|
+
attr_reader :at, :klass, :thread
|
59
58
|
|
60
59
|
def initialize thread, at, klass
|
61
60
|
@thread, @at, @klass = thread, at, klass
|
62
|
-
@defused
|
61
|
+
@defused = false
|
63
62
|
end
|
64
63
|
|
65
64
|
def <=>(other)
|
@@ -75,12 +74,9 @@ module FrugalTimeout
|
|
75
74
|
@@mutex.synchronize { @defused }
|
76
75
|
end
|
77
76
|
|
78
|
-
def enforceTimeout
|
77
|
+
def enforceTimeout
|
79
78
|
@@mutex.synchronize {
|
80
|
-
|
81
|
-
|
82
|
-
filter[@thread] = true
|
83
|
-
@thread.raise @exception, 'execution expired'
|
79
|
+
@thread.raise @klass, 'execution expired' unless @defused
|
84
80
|
}
|
85
81
|
end
|
86
82
|
end
|
@@ -95,6 +91,11 @@ module FrugalTimeout
|
|
95
91
|
@onNewNearestRequest, @requests = proc {}, SortedQueue.new
|
96
92
|
end
|
97
93
|
|
94
|
+
def defuse_thread! thread
|
95
|
+
@requests.each { |r| r.defuse! if r.thread == thread }
|
96
|
+
end
|
97
|
+
private :defuse_thread!
|
98
|
+
|
98
99
|
def onNewNearestRequest &b
|
99
100
|
@onNewNearestRequest = b
|
100
101
|
end
|
@@ -102,13 +103,19 @@ module FrugalTimeout
|
|
102
103
|
# Purge and enforce expired timeouts. Only enforce once for each thread,
|
103
104
|
# even if multiple timeouts for that thread expire at once.
|
104
105
|
def purgeExpired
|
105
|
-
filter, now = {}, MonotonicTime.now
|
106
|
-
@requests.reject_and_get! { |r| r.at <= now }.each { |r|
|
107
|
-
r.enforceTimeout filter
|
108
|
-
}
|
109
|
-
|
106
|
+
expiredRequests, filter, now = nil, {}, MonotonicTime.now
|
110
107
|
@requests.synchronize {
|
111
|
-
@
|
108
|
+
@requests.reject_and_get! { |r| r.at <= now }.each { |r|
|
109
|
+
next if filter[r.thread]
|
110
|
+
|
111
|
+
r.enforceTimeout
|
112
|
+
defuse_thread! r.thread
|
113
|
+
filter[r.thread] = true
|
114
|
+
}
|
115
|
+
|
116
|
+
# It's necessary to call onNewNearestRequest inside synchronize as other
|
117
|
+
# threads may #queue requests.
|
118
|
+
@onNewNearestRequest.call @requests.first unless @requests.empty?
|
112
119
|
}
|
113
120
|
end
|
114
121
|
|
@@ -123,56 +130,73 @@ module FrugalTimeout
|
|
123
130
|
end
|
124
131
|
|
125
132
|
# {{{1 SleeperNotifier
|
133
|
+
# Executes callback when a request expires.
|
134
|
+
# 1. Set callback to execute with #onExpiry=.
|
135
|
+
# 2. Set expiry time with #expireAt.
|
136
|
+
# 3. After the expiry time comes, execute the callback.
|
137
|
+
#
|
138
|
+
# It's possible to set a new expiry time before the time set previously
|
139
|
+
# expires. In this case, processing of the old request stops and the new
|
140
|
+
# request processing starts.
|
126
141
|
class SleeperNotifier # :nodoc:
|
127
142
|
include MonitorMixin
|
128
143
|
|
144
|
+
DO_NOTHING = proc {}
|
145
|
+
|
129
146
|
def initialize
|
130
147
|
super()
|
131
|
-
@condVar, @
|
148
|
+
@condVar, @expireAt, @onExpiry = new_cond, nil, DO_NOTHING
|
132
149
|
|
133
150
|
@thread = Thread.new {
|
134
151
|
loop {
|
135
|
-
@onExpiry.call if synchronize {
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
@request = nil
|
141
|
-
true
|
152
|
+
synchronize { @onExpiry }.call if synchronize {
|
153
|
+
# Sleep forever until a request comes in.
|
154
|
+
unless @expireAt
|
155
|
+
wait
|
156
|
+
next
|
142
157
|
end
|
158
|
+
|
159
|
+
timeLeft = calcTimeLeft
|
160
|
+
disposeOfRequest
|
161
|
+
elapsedTime = MonotonicTime.measure { wait timeLeft }
|
162
|
+
|
163
|
+
elapsedTime >= timeLeft
|
143
164
|
}
|
144
165
|
}
|
145
166
|
}
|
146
167
|
ObjectSpace.define_finalizer self, proc { @thread.kill }
|
147
168
|
end
|
148
169
|
|
149
|
-
def
|
170
|
+
def onExpiry &b
|
171
|
+
synchronize { @onExpiry = b || DO_NOTHING }
|
172
|
+
end
|
173
|
+
|
174
|
+
def expireAt time
|
150
175
|
synchronize {
|
151
|
-
|
176
|
+
@expireAt = time
|
177
|
+
signalThread
|
178
|
+
}
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
152
182
|
|
153
|
-
|
183
|
+
def calcTimeLeft
|
184
|
+
synchronize {
|
185
|
+
delay = @expireAt - MonotonicTime.now
|
154
186
|
delay < 0 ? 0 : delay
|
155
187
|
}
|
156
188
|
end
|
157
|
-
private :latestDelay
|
158
189
|
|
159
|
-
def
|
160
|
-
@
|
190
|
+
def disposeOfRequest
|
191
|
+
@expireAt = nil
|
161
192
|
end
|
162
|
-
private :notify
|
163
193
|
|
164
|
-
def
|
165
|
-
@
|
194
|
+
def signalThread
|
195
|
+
@condVar.signal
|
166
196
|
end
|
167
197
|
|
168
|
-
|
169
|
-
|
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
|
-
}
|
198
|
+
def wait sec=nil
|
199
|
+
@condVar.wait sec
|
176
200
|
end
|
177
201
|
end
|
178
202
|
|
@@ -185,6 +209,10 @@ module FrugalTimeout
|
|
185
209
|
@array, @unsorted = storage, false
|
186
210
|
end
|
187
211
|
|
212
|
+
def each &b
|
213
|
+
synchronize { @array.each &b }
|
214
|
+
end
|
215
|
+
|
188
216
|
def empty?
|
189
217
|
synchronize { @array.empty? }
|
190
218
|
end
|
@@ -249,7 +277,7 @@ module FrugalTimeout
|
|
249
277
|
@requestQueue = RequestQueue.new
|
250
278
|
sleeper = SleeperNotifier.new
|
251
279
|
@requestQueue.onNewNearestRequest { |request|
|
252
|
-
sleeper.
|
280
|
+
sleeper.expireAt request.at
|
253
281
|
}
|
254
282
|
sleeper.onExpiry { @requestQueue.purgeExpired }
|
255
283
|
|
@@ -263,18 +291,22 @@ module FrugalTimeout
|
|
263
291
|
end'
|
264
292
|
end
|
265
293
|
|
294
|
+
def self.on_ensure &b # :nodoc:
|
295
|
+
@onEnsure = b
|
296
|
+
end
|
297
|
+
|
266
298
|
# Same as Timeout.timeout()
|
267
299
|
def self.timeout sec, klass=Error
|
268
300
|
return yield sec if sec.nil? || sec <= 0
|
269
301
|
|
270
|
-
|
302
|
+
innerException = Class.new Timeout::ExitException
|
303
|
+
request = @requestQueue.queue(sec, innerException)
|
271
304
|
begin
|
272
305
|
yield sec
|
273
|
-
rescue
|
274
|
-
raise
|
275
|
-
|
276
|
-
raise request.klass, e.message, e.backtrace
|
306
|
+
rescue innerException => e
|
307
|
+
raise klass, e.message, e.backtrace
|
277
308
|
ensure
|
309
|
+
@onEnsure.call if @onEnsure
|
278
310
|
request.defuse!
|
279
311
|
end
|
280
312
|
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.9
|
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:
|
12
|
+
date: 2014-01-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -43,22 +43,6 @@ dependencies:
|
|
43
43
|
- - ~>
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '1.2'
|
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
46
|
description: Timeout.timeout replacement that uses only 1 thread
|
63
47
|
email: ledestin@gmail.com
|
64
48
|
executables: []
|