frugal_timeout 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/frugal_timeout.rb +77 -45
  2. metadata +2 -18
@@ -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, :exception, :klass, :thread
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, @exception = false, Class.new(Timeout::ExitException)
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 filter=NullObject.new {}
77
+ def enforceTimeout
79
78
  @@mutex.synchronize {
80
- return if @defused || filter.has_key?(@thread)
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
- @onNewNearestRequest.call(@requests.first) unless @requests.empty?
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, @onExpiry, @request = new_cond, proc {}, nil
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
- sleepFor = latestDelay
137
- sleptFor = MonotonicTime.measure { @condVar.wait sleepFor }
138
-
139
- if sleepFor && sleptFor >= sleepFor
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 latestDelay
170
+ def onExpiry &b
171
+ synchronize { @onExpiry = b || DO_NOTHING }
172
+ end
173
+
174
+ def expireAt time
150
175
  synchronize {
151
- return unless @request
176
+ @expireAt = time
177
+ signalThread
178
+ }
179
+ end
180
+
181
+ private
152
182
 
153
- delay = @request.at - MonotonicTime.now
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 notify
160
- @condVar.signal
190
+ def disposeOfRequest
191
+ @expireAt = nil
161
192
  end
162
- private :notify
163
193
 
164
- def onExpiry &b
165
- @onExpiry = b
194
+ def signalThread
195
+ @condVar.signal
166
196
  end
167
197
 
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
- }
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.sleepUntilExpires request
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
- request = @requestQueue.queue(sec, klass)
302
+ innerException = Class.new Timeout::ExitException
303
+ request = @requestQueue.queue(sec, innerException)
271
304
  begin
272
305
  yield sec
273
- rescue request.exception => e
274
- raise unless e.is_a? Timeout::ExitException
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.8
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: 2013-12-20 00:00:00.000000000 Z
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: []