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.
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: []