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