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.
Files changed (2) hide show
  1. data/lib/frugal_timeout.rb +169 -83
  2. metadata +19 -3
@@ -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 2 threads
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
- def done!
66
- @@mutex.synchronize { @done = true }
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 done?
70
- @@mutex.synchronize { @done }
74
+ def defused?
75
+ @@mutex.synchronize { @defused }
71
76
  end
72
77
 
73
- def enforceTimeout
74
- @thread.raise @klass || Error, 'execution expired' unless done?
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
- def initialize notifyQueue
81
- @notifyQueue = notifyQueue
82
- @latestDelay, @mutex = nil, Mutex.new
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
- unless sleepFor = latestDelay
87
- sleep
88
- else
89
- sleptFor = MonotonicTime.measure { sleep(sleepFor) }
90
- end
91
- synchronize {
92
- @notifyQueue.push :expired if sleepFor && sleptFor >= sleepFor
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
- tmp = @latestDelay
102
- @latestDelay = nil
103
- tmp
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 notifyAfter sec
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
- sleep 0.01 until @thread.status == 'sleep'
111
- @latestDelay = sec
112
- @thread.wakeup
198
+ sort!
199
+ @array.last
113
200
  }
114
201
  end
115
202
 
116
- def synchronize &b
117
- @mutex.synchronize &b
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
- @in = Queue.new
124
- @sleeper = SleeperNotifier.new @in
125
-
126
- # {{{2 Timeout request and expiration processing thread
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=nil, &b
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=nil
185
- return yield sec if sec == nil || sec <= 0
267
+ def self.timeout sec, klass=Error
268
+ return yield sec if sec.nil? || sec <= 0
186
269
 
187
- @in.push request = Request.new(Thread.current, MonotonicTime.now + sec,
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.done! unless $!.is_a? FrugalTimeout::Error
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.7
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-10-20 00:00:00.000000000 Z
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
- description: Timeout.timeout replacement that uses only 2 threads
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: []