utilrb 2.0.0 → 2.0.1
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/Manifest.txt +12 -4
- data/ext/utilrb/extconf.rb +5 -5
- data/ext/utilrb/proc.c +3 -3
- data/ext/utilrb/utilrb.cc +0 -2
- data/ext/utilrb/weakref.cc +3 -3
- data/lib/utilrb/common.rb +1 -1
- data/lib/utilrb/event_loop.rb +969 -0
- data/lib/utilrb/logger/silent.rb +10 -0
- data/lib/utilrb/models/inherited_enumerable.rb +341 -0
- data/lib/utilrb/models/registration.rb +115 -0
- data/lib/utilrb/pathname/find_matching_parent.rb +20 -0
- data/lib/utilrb/pathname.rb +3 -0
- data/lib/utilrb/qt/mime_data/mime_data.rb +18 -0
- data/lib/utilrb/qt/variant/from_ruby.rb +30 -0
- data/lib/utilrb/thread_pool.rb +592 -0
- data/test/test_array.rb +1 -1
- data/test/test_dir.rb +1 -1
- data/test/test_enumerable.rb +1 -1
- data/test/test_event_loop.rb +1 -1
- data/test/test_exception.rb +1 -1
- data/test/test_gc.rb +1 -1
- data/test/test_hash.rb +1 -1
- data/test/test_kernel.rb +3 -13
- data/test/test_misc.rb +1 -1
- data/test/test_models.rb +1 -1
- data/test/test_module.rb +1 -1
- data/test/test_object.rb +1 -1
- data/test/test_objectstats.rb +1 -1
- data/test/test_proc.rb +1 -1
- data/test/test_set.rb +1 -1
- data/test/test_thread_pool.rb +1 -1
- data/test/test_time.rb +1 -1
- data/test/test_unbound_method.rb +1 -1
- metadata +30 -25
- data/ext/utilrb/ruby_internals-1.8.h +0 -72
- data/ext/utilrb/ruby_internals-1.9.h +0 -71
- data/ext/utilrb/swap.cc +0 -31
- data/lib/utilrb/kernel/swap.rb +0 -2
@@ -0,0 +1,969 @@
|
|
1
|
+
require 'utilrb/thread_pool'
|
2
|
+
|
3
|
+
|
4
|
+
module Utilrb
|
5
|
+
# Simple event loop which supports timers and defers blocking operations to
|
6
|
+
# a thread pool those results are queued and being processed by the event
|
7
|
+
# loop thread at the end of each step.
|
8
|
+
#
|
9
|
+
# All events must be code blocks which will be executed at the end of each step.
|
10
|
+
# There is no support for filtering or event propagations.
|
11
|
+
#
|
12
|
+
# For an easy integration of ruby classes into the event loop the {Forwardable#def_event_loop_delegator}
|
13
|
+
# can be used.
|
14
|
+
#
|
15
|
+
# @example Example for using the EventLoop
|
16
|
+
# event_loop = EventLoop.new
|
17
|
+
# event_loop.once do
|
18
|
+
# puts "called once"
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# event_loop.every(1.0) do
|
22
|
+
# puts "called every second"
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# callback = Proc.new |result|
|
26
|
+
# puts result
|
27
|
+
# end
|
28
|
+
# event_loop.defer callback do
|
29
|
+
# sleep 2
|
30
|
+
# "result from the worker thread #{Thread.current}"
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# event_loop.exec
|
34
|
+
#
|
35
|
+
# @author Alexander Duda <Alexander.Duda@dfki.de>
|
36
|
+
class EventLoop
|
37
|
+
# Timer for the {EventLoop} which supports single shot and periodic activation
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# loop = EventLoop.new
|
41
|
+
# timer = EventLoop.every(0.1) do
|
42
|
+
# puts 123
|
43
|
+
# end
|
44
|
+
# loop.exec
|
45
|
+
class Timer
|
46
|
+
attr_accessor :single_shot
|
47
|
+
attr_accessor :period
|
48
|
+
attr_accessor :event_loop
|
49
|
+
attr_reader :result
|
50
|
+
attr_accessor :doc
|
51
|
+
|
52
|
+
# A timer
|
53
|
+
#
|
54
|
+
# @param [EventLoop] event_loop the {EventLoop} the timer belongs to
|
55
|
+
# @param[Float] period The period of the timer in seconds.
|
56
|
+
# @param[Boolean] single_shot if true the timer will fire only once
|
57
|
+
# @param[#call] block The code block which will be executed each time the timer fires
|
58
|
+
# @see EventLoop#once
|
59
|
+
def initialize(event_loop,period=0,single_shot=false,&block)
|
60
|
+
@block = block
|
61
|
+
@event_loop = event_loop
|
62
|
+
@last_call = Time.now
|
63
|
+
@period = period
|
64
|
+
@single_shot = single_shot
|
65
|
+
@stopped = true
|
66
|
+
@doc = Kernel.caller.find do |s|
|
67
|
+
!(%r"#{Regexp.quote(__FILE__)}"o =~ s) && !(s =~ /^\/usr\/.+/)
|
68
|
+
end.to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
# Cancels the timer. If it is not running it will do nothing
|
72
|
+
def cancel
|
73
|
+
@stopped = true
|
74
|
+
@event_loop.cancel_timer self
|
75
|
+
end
|
76
|
+
|
77
|
+
def single_shot?
|
78
|
+
@single_shot
|
79
|
+
end
|
80
|
+
|
81
|
+
def stopped?
|
82
|
+
@stopped
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns true if the timer is currently running.
|
86
|
+
#
|
87
|
+
# @return [boolean]
|
88
|
+
def running?
|
89
|
+
@event_loop.timer? self
|
90
|
+
end
|
91
|
+
|
92
|
+
# Starts the timer by adding itself to the EventLoop the timer
|
93
|
+
# belongs to. If no period is given the one which was given during
|
94
|
+
# initializing will be used.
|
95
|
+
#
|
96
|
+
# @param [Float] period The period in seconds
|
97
|
+
# @param [TrueClass,FalseClass] instantly If set to true the timer instantly runs otherwise
|
98
|
+
# the timer waits until the first period passed.
|
99
|
+
# @raise [ArgumentError] if no period is specified
|
100
|
+
# @return [Timer]
|
101
|
+
def start(period = @period,instantly = true)
|
102
|
+
cancel
|
103
|
+
@stopped = false
|
104
|
+
@period = period
|
105
|
+
raise ArgumentError,"no period is given" unless @period
|
106
|
+
@last_call = if instantly
|
107
|
+
Time.at(0)
|
108
|
+
else
|
109
|
+
Time.now
|
110
|
+
end
|
111
|
+
@event_loop.add_timer self
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns true if the timer should fire now. This is called by the
|
116
|
+
# EventLoop to check if the timer elapsed.
|
117
|
+
#
|
118
|
+
# @param [Time] time The time used for checking
|
119
|
+
# @return [Boolean}
|
120
|
+
def timeout?(time = Time.now)
|
121
|
+
if(time-@last_call).to_f >= @period
|
122
|
+
true
|
123
|
+
else
|
124
|
+
false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns true if the timer is a single shot timer.
|
129
|
+
#
|
130
|
+
# @return [Boolean}
|
131
|
+
def single_shot?
|
132
|
+
@single_shot == true
|
133
|
+
end
|
134
|
+
|
135
|
+
# Executes the code block tight to the timer and
|
136
|
+
# saves a time stamp.
|
137
|
+
#
|
138
|
+
# @param [Time] time The time stamp
|
139
|
+
def call(time = Time.now)
|
140
|
+
reset(time)
|
141
|
+
@result = @block.call
|
142
|
+
end
|
143
|
+
|
144
|
+
# Resets the timer internal time to the given one.
|
145
|
+
#
|
146
|
+
# @param [Time] time the time
|
147
|
+
def reset(time = Time.now)
|
148
|
+
@last_call = time
|
149
|
+
end
|
150
|
+
|
151
|
+
alias :stop :cancel
|
152
|
+
end
|
153
|
+
|
154
|
+
# An event loop event
|
155
|
+
class Event
|
156
|
+
def initialize(block)
|
157
|
+
@block = block
|
158
|
+
@ignore = false
|
159
|
+
end
|
160
|
+
|
161
|
+
def call
|
162
|
+
@block.call
|
163
|
+
end
|
164
|
+
|
165
|
+
# If called the event will be ignored and
|
166
|
+
# removed from all internal event queues.
|
167
|
+
def ignore!
|
168
|
+
@ignore = true
|
169
|
+
end
|
170
|
+
|
171
|
+
def ignore?
|
172
|
+
@ignore
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.cleanup_backtrace(&block)
|
177
|
+
block.call
|
178
|
+
rescue
|
179
|
+
$@.delete_if{|s| %r"#{Regexp.quote(__FILE__)}"o =~ s}
|
180
|
+
::Kernel::raise
|
181
|
+
end
|
182
|
+
|
183
|
+
# Underlying thread pool used to defer work.
|
184
|
+
#
|
185
|
+
# @return [Utilrb::ThreadPool]
|
186
|
+
attr_reader :thread_pool
|
187
|
+
|
188
|
+
# A new EventLoop
|
189
|
+
def initialize
|
190
|
+
@mutex = Mutex.new
|
191
|
+
@events = Queue.new # stores all events for the next step
|
192
|
+
@timers = Set.new # stores all timers
|
193
|
+
@every_cylce_events = Set.new # stores all events which are added to @events each step
|
194
|
+
@on_error = {} # stores on error callbacks
|
195
|
+
@errors = Queue.new # stores errors which will be re raised at the end of the step
|
196
|
+
@number_of_events_to_process = 0 # number of events which are processed in the current step
|
197
|
+
@thread_pool = ThreadPool.new
|
198
|
+
@thread = Thread.current #the event loop thread
|
199
|
+
@stop = nil
|
200
|
+
end
|
201
|
+
|
202
|
+
# Integrates a blocking operation call into the EventLoop like {Utilrb::EventLoop#defer}
|
203
|
+
# but has a more suitable syntax for deferring a method call
|
204
|
+
#
|
205
|
+
# async method(:my_method) do |result,exception|
|
206
|
+
# if exception
|
207
|
+
# raise exception
|
208
|
+
# else
|
209
|
+
# puts result
|
210
|
+
# end
|
211
|
+
# end
|
212
|
+
#
|
213
|
+
# @param [#call] work The proc which will be deferred
|
214
|
+
# @yield [result] The callback
|
215
|
+
# @yield [result,exception] The callback
|
216
|
+
# @return [Utilrb::ThreadPool::Task] The thread pool task.
|
217
|
+
def async(work,*args,&callback)
|
218
|
+
async_with_options(work,Hash.new,*args,&callback)
|
219
|
+
end
|
220
|
+
|
221
|
+
# (see #async)
|
222
|
+
# @param [Hash] options The options
|
223
|
+
# @option (see #defer)
|
224
|
+
def async_with_options(work,options=Hash.new,*args,&callback)
|
225
|
+
options[:callback] = callback
|
226
|
+
defer(options,*args,&work)
|
227
|
+
end
|
228
|
+
|
229
|
+
# (see ThreadPool#sync)
|
230
|
+
def sync(sync_key,*args,&block)
|
231
|
+
thread_pool.sync(sync_key,*args,&block)
|
232
|
+
end
|
233
|
+
|
234
|
+
def pretty_print(pp) # :nodoc:
|
235
|
+
pp.text "EventLoop "
|
236
|
+
end
|
237
|
+
|
238
|
+
# Integrates a blocking operation call like {Utilrb::EventLoop#async}
|
239
|
+
# but automatically re queues the call if period was passed and the
|
240
|
+
# task was finished by the worker thread. This means it will never re
|
241
|
+
# queue the call if the task blocks for ever and it will never simultaneously
|
242
|
+
# defer the call to more than one worker thread.
|
243
|
+
#
|
244
|
+
# @param [Hash] options The options
|
245
|
+
# @option options [Float] :period The period
|
246
|
+
# @option options [Boolean] :start Starts the timer right away (default = true)
|
247
|
+
# @param [#call] work The proc which will be deferred
|
248
|
+
# @param (see #defer)
|
249
|
+
# @option (see #defer)
|
250
|
+
# @return [EventLoop::Timer] The thread pool task.
|
251
|
+
def async_every(work,options=Hash.new,*args, &callback)
|
252
|
+
options, async_opt = Kernel.filter_options(options,:period,:start => true)
|
253
|
+
period = options[:period]
|
254
|
+
raise ArgumentError,"No period given" unless period
|
255
|
+
task = nil
|
256
|
+
every period ,options[:start] do
|
257
|
+
if !task
|
258
|
+
task = async_with_options(work,async_opt,*args,&callback)
|
259
|
+
elsif task.finished?
|
260
|
+
add_task task
|
261
|
+
end
|
262
|
+
task
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Integrates a blocking operation call into the EventLoop by
|
267
|
+
# executing it from a different thread. The given callback
|
268
|
+
# will be called from the EventLoop thread while processing its events after
|
269
|
+
# the call returned.
|
270
|
+
#
|
271
|
+
# If the callback has an arity of 2 the exception will be passed to the
|
272
|
+
# callback as second parameter in an event of an error. The error is
|
273
|
+
# also passed to the error handlers of the even loop, but it will not
|
274
|
+
# be re raised if the error is marked as known
|
275
|
+
#
|
276
|
+
# To overwrite an error the callback can return :ignore_error or
|
277
|
+
# a new instance of an error in an event of an error. In this
|
278
|
+
# case the error handlers of the event loop will not be called
|
279
|
+
# or called with the new error instance.
|
280
|
+
#
|
281
|
+
# @example ignore a error
|
282
|
+
# callback = Proc.new do |r,e|
|
283
|
+
# if e
|
284
|
+
# :ignore_error
|
285
|
+
# else
|
286
|
+
# puts r
|
287
|
+
# end
|
288
|
+
# end
|
289
|
+
# defer({:callback => callback}) do
|
290
|
+
# raise
|
291
|
+
# end
|
292
|
+
#
|
293
|
+
# @param [Hash] options The options
|
294
|
+
# @option (see ThreadPool::Task#initialize)
|
295
|
+
# @option options [Proc] :callback The callback
|
296
|
+
# @option options [class] :known_errors Known erros which will be rescued
|
297
|
+
# @option options [Proc] :on_error Callback which is called when an error occured
|
298
|
+
#
|
299
|
+
# @param (see ThreadPool::Task#initialize)
|
300
|
+
# @return [ThreadPool::Task] The thread pool task.
|
301
|
+
def defer(options=Hash.new,*args,&block)
|
302
|
+
options, task_options = Kernel.filter_options(options,{:callback => nil,:known_errors => [],:on_error => nil})
|
303
|
+
callback = options[:callback]
|
304
|
+
error_callback = options[:on_error]
|
305
|
+
known_errors = Array(options[:known_errors])
|
306
|
+
|
307
|
+
task = Utilrb::ThreadPool::Task.new(task_options,*args,&block)
|
308
|
+
# ensures that user callback is called from main thread and not from worker threads
|
309
|
+
if callback
|
310
|
+
task.callback do |result,exception|
|
311
|
+
once do
|
312
|
+
if callback.arity == 1
|
313
|
+
callback.call result if !exception
|
314
|
+
else
|
315
|
+
e = callback.call result,exception
|
316
|
+
#check if the error was overwritten in the
|
317
|
+
#case of an error
|
318
|
+
exception = if exception
|
319
|
+
if e.is_a?(Symbol) && e == :ignore_error
|
320
|
+
nil
|
321
|
+
elsif e.is_a? Exception
|
322
|
+
e
|
323
|
+
else
|
324
|
+
exception
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
if exception
|
329
|
+
error_callback.call(exception) if error_callback
|
330
|
+
raises = !known_errors.any? {|error| exception.is_a?(error)}
|
331
|
+
handle_error(exception,raises)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
else
|
336
|
+
task.callback do |result,exception|
|
337
|
+
if exception
|
338
|
+
raises = !known_errors.find {|error| exception.is_a?(error)}
|
339
|
+
once do
|
340
|
+
error_callback.call(exception) if error_callback
|
341
|
+
handle_error(exception,raises)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
@mutex.synchronize do
|
347
|
+
@thread_pool << task
|
348
|
+
end
|
349
|
+
task
|
350
|
+
end
|
351
|
+
|
352
|
+
# Executes the given block in the next step from the event loop thread.
|
353
|
+
# Returns a Timer object if a delay is set otherwise an handler to the
|
354
|
+
# Event which was queued.
|
355
|
+
#
|
356
|
+
# @yield [] The code block.
|
357
|
+
# @return [Utilrb::EventLoop::Timer,Event]
|
358
|
+
def once(delay=nil,&block)
|
359
|
+
raise ArgumentError "no block given" unless block
|
360
|
+
if delay && delay > 0
|
361
|
+
timer = Timer.new(self,delay,true,&block)
|
362
|
+
timer.start
|
363
|
+
else
|
364
|
+
add_event(Event.new(block))
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
# Calls the give block in the event loop thread. If the current thread
|
369
|
+
# is the event loop thread it will execute it right a way and returns
|
370
|
+
# the result of the code block call. Otherwise, it returns an handler to
|
371
|
+
# the Event which was queued.
|
372
|
+
#
|
373
|
+
#@return [Event,Object]
|
374
|
+
def call(&block)
|
375
|
+
if thread?
|
376
|
+
block.call
|
377
|
+
else
|
378
|
+
once(&block)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
# Returns true if events are queued.
|
383
|
+
#
|
384
|
+
# @return [Boolean]
|
385
|
+
def events?
|
386
|
+
!@events.empty? || !@errors.empty?
|
387
|
+
end
|
388
|
+
|
389
|
+
# Adds a timer to the event loop which will execute
|
390
|
+
# the given code block with the given period from the
|
391
|
+
# event loop thread.
|
392
|
+
#
|
393
|
+
# @param [Float] period The period of the timer in seconds
|
394
|
+
# @parma [Boolean] start Startet the timerright away.
|
395
|
+
# @yield The code block.
|
396
|
+
# @return [Utilrb::EventLoop::Timer]
|
397
|
+
def every(period,start=true,&block)
|
398
|
+
timer = Timer.new(self,period,&block)
|
399
|
+
timer.start if start # adds itself to the event loop
|
400
|
+
timer
|
401
|
+
end
|
402
|
+
|
403
|
+
# Executes the given block every step from the event loop thread.
|
404
|
+
#
|
405
|
+
# @return [Event] The event
|
406
|
+
def every_step(&block)
|
407
|
+
add_event Event.new(block),true
|
408
|
+
end
|
409
|
+
|
410
|
+
# Errors caught during event loop callbacks are forwarded to
|
411
|
+
# registered code blocks. The code block is called from
|
412
|
+
# the event loop thread.
|
413
|
+
#
|
414
|
+
# @param @error_class The error class the block should be called for
|
415
|
+
# @yield [exception] The code block
|
416
|
+
def on_error(error_class,&block)
|
417
|
+
@mutex.synchronize do
|
418
|
+
@on_error[error_class] ||= []
|
419
|
+
@on_error[error_class] << block
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
# Errors caught during event loop callbacks are forwarded to
|
424
|
+
# registered code blocks. The code blocks are called from
|
425
|
+
# the event loop thread.
|
426
|
+
#
|
427
|
+
# @param @error_classes The error classes the block should be called for
|
428
|
+
# @yield [exception] The code block
|
429
|
+
def on_errors(*error_classes,&block)
|
430
|
+
error_classes.flatten!
|
431
|
+
error_classes.each do |error_class|
|
432
|
+
on_error(error_class,&block)
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
# Raises if the current thread is not the event loop thread (by default
|
437
|
+
# the one the event loop was started from).
|
438
|
+
#
|
439
|
+
# @raise [RuntimeError]
|
440
|
+
def validate_thread
|
441
|
+
raise "current thread is not the event loop thread" if !thread?
|
442
|
+
end
|
443
|
+
|
444
|
+
# Returns true if the current thread is the
|
445
|
+
# event loop thread.
|
446
|
+
#
|
447
|
+
# @return [Boolean]
|
448
|
+
def thread?
|
449
|
+
@mutex.synchronize do
|
450
|
+
if Thread.current == @thread
|
451
|
+
true
|
452
|
+
else
|
453
|
+
false
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# Sets the event loop thread. By default it is set to the one
|
459
|
+
# the EventLoop was started from.
|
460
|
+
#
|
461
|
+
# @param[Thread] thread The thread
|
462
|
+
def thread=(thread)
|
463
|
+
@mutex.synchronize do
|
464
|
+
@thread = thread
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
# Returns true if the given timer is running.
|
469
|
+
#
|
470
|
+
# @param [Timer] timer The timer.
|
471
|
+
# @return [Boolean]
|
472
|
+
def timer?(timer)
|
473
|
+
@mutex.synchronize do
|
474
|
+
@timers.include? timer
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
# Returns all currently running timers.
|
479
|
+
#
|
480
|
+
# @return Array<Timer>
|
481
|
+
def timers
|
482
|
+
@mutex.synchronize do
|
483
|
+
@timers.dup
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
# Cancels the given timer if it is running otherwise
|
488
|
+
# it does nothing.
|
489
|
+
#
|
490
|
+
# @param [Timer] timer The timer
|
491
|
+
def cancel_timer(timer)
|
492
|
+
@mutex.synchronize do
|
493
|
+
@timers.delete timer
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
# Resets all timers to fire not before their hole
|
498
|
+
# period is passed counting from the given point in time.
|
499
|
+
#
|
500
|
+
# @param [Time] time The time
|
501
|
+
def reset_timers(time = Time.now)
|
502
|
+
@mutex.synchronize do
|
503
|
+
@timers.each do |timer|
|
504
|
+
timer.reset time
|
505
|
+
end
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
# Starts the event loop with the given period. If a code
|
510
|
+
# block is given it will be executed at the end of each step.
|
511
|
+
# This method will block until stop is called
|
512
|
+
#
|
513
|
+
# @param [Float] period The period
|
514
|
+
# @yield The code block
|
515
|
+
def exec(period=0.05,&block)
|
516
|
+
@stop = false
|
517
|
+
reset_timers
|
518
|
+
while !@stop
|
519
|
+
last_step = Time.now
|
520
|
+
step(last_step,&block)
|
521
|
+
diff = (Time.now-last_step).to_f
|
522
|
+
sleep(period-diff) if diff < period && !@stop
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
# Stops the EventLoop after [#exec] was called.
|
527
|
+
def stop
|
528
|
+
@stop = true
|
529
|
+
end
|
530
|
+
|
531
|
+
# Steps with the given period until the given
|
532
|
+
# block returns true.
|
533
|
+
#
|
534
|
+
# @param [Float] period The period
|
535
|
+
# @param [Float] timeout The timeout in seconds
|
536
|
+
# @yieldreturn [Boolean]
|
537
|
+
def wait_for(period=0.05,timeout=nil,&block)
|
538
|
+
start = Time.now
|
539
|
+
old_stop = @stop
|
540
|
+
exec period do
|
541
|
+
stop if block.call
|
542
|
+
if timeout && timeout <= (Time.now-start).to_f
|
543
|
+
raise RuntimeError,"Timeout during wait_for"
|
544
|
+
end
|
545
|
+
end
|
546
|
+
@stop = old_stop
|
547
|
+
end
|
548
|
+
|
549
|
+
# Steps with the given period until all
|
550
|
+
# worker thread are waiting for work
|
551
|
+
#
|
552
|
+
# @param [Float] period Ther period
|
553
|
+
# @param (@see #step)
|
554
|
+
def steps(period = 0.05,max_time=1.0,&block)
|
555
|
+
start = Time.now
|
556
|
+
begin
|
557
|
+
last_step = Time.now
|
558
|
+
step(last_step,&block)
|
559
|
+
time = Time.now
|
560
|
+
break if max_time && max_time <= (time-start).to_f
|
561
|
+
diff = (time-last_step).to_f
|
562
|
+
sleep(period-diff) if diff < period && !@stop
|
563
|
+
end while (thread_pool.process? || events?)
|
564
|
+
end
|
565
|
+
|
566
|
+
# (see ThreadPool#backlog)
|
567
|
+
def backlog
|
568
|
+
thread_pool.backlog
|
569
|
+
end
|
570
|
+
|
571
|
+
# Shuts down the thread pool
|
572
|
+
def shutdown()
|
573
|
+
thread_pool.shutdown()
|
574
|
+
end
|
575
|
+
|
576
|
+
def reraise_error(error)
|
577
|
+
raise error, error.message, error.backtrace + caller(1)
|
578
|
+
end
|
579
|
+
|
580
|
+
# Handles all current events and timers. If a code
|
581
|
+
# block is given it will be executed at the end.
|
582
|
+
#
|
583
|
+
# @param [Time] time The time the step is executed for.
|
584
|
+
# @yield The code block
|
585
|
+
def step(time = Time.now,&block)
|
586
|
+
validate_thread
|
587
|
+
reraise_error(@errors.shift) if !@errors.empty?
|
588
|
+
|
589
|
+
#copy all work otherwise it would not be allowed to
|
590
|
+
#call any event loop functions from a timer
|
591
|
+
timers,call = @mutex.synchronize do
|
592
|
+
@every_cylce_events.delete_if &:ignore?
|
593
|
+
@every_cylce_events.each do |event|
|
594
|
+
add_event event
|
595
|
+
end
|
596
|
+
|
597
|
+
# check all timers
|
598
|
+
temp_timers = @timers.find_all do |timer|
|
599
|
+
timer.timeout?(time)
|
600
|
+
end
|
601
|
+
# delete single shot timer which elapsed
|
602
|
+
@timers -= temp_timers.find_all(&:single_shot?)
|
603
|
+
[temp_timers,block]
|
604
|
+
end
|
605
|
+
|
606
|
+
# handle all current events but not the one which are added during processing.
|
607
|
+
# Step is recursively be called if wait_for is used insight an event code block.
|
608
|
+
# To make sure that all events and timer are processed in the right order
|
609
|
+
# @number_of_events_to_process and a second timeout check is used.
|
610
|
+
@number_of_events_to_process = [@events.size,@number_of_events_to_process].max
|
611
|
+
while @number_of_events_to_process > 0
|
612
|
+
event = @events.pop
|
613
|
+
@number_of_events_to_process -= 1
|
614
|
+
handle_errors{event.call} unless event.ignore?
|
615
|
+
end
|
616
|
+
timers.each do |timer|
|
617
|
+
next if timer.stopped?
|
618
|
+
handle_errors{timer.call(time)} if timer.timeout?(time)
|
619
|
+
end
|
620
|
+
handle_errors{call.call} if call
|
621
|
+
reraise_error(@errors.shift) if !@errors.empty?
|
622
|
+
|
623
|
+
#allow thread pool to take over
|
624
|
+
Thread.pass
|
625
|
+
end
|
626
|
+
|
627
|
+
# Adds a timer to the event loop
|
628
|
+
#
|
629
|
+
# @param [Timer] timer The timer.
|
630
|
+
def add_timer(timer)
|
631
|
+
@mutex.synchronize do
|
632
|
+
raise "timer #{timer}:#{timer.doc} was already added!" if @timers.include?(timer)
|
633
|
+
@timers << timer
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
# Adds an Event to the event loop
|
638
|
+
#
|
639
|
+
# @param [Event] event The event
|
640
|
+
# @param [Boolean] every_step Automatically added for every step
|
641
|
+
def add_event(event,every_step = false)
|
642
|
+
raise ArgumentError "cannot add event which is ignored." if event.ignore?
|
643
|
+
if every_step
|
644
|
+
@mutex.synchronize do
|
645
|
+
@every_cylce_events << event
|
646
|
+
end
|
647
|
+
else
|
648
|
+
@events << event
|
649
|
+
end
|
650
|
+
event
|
651
|
+
end
|
652
|
+
|
653
|
+
# Adds a task to the thread pool
|
654
|
+
#
|
655
|
+
# @param [ThreadPool::Task] task The task.
|
656
|
+
def add_task(task)
|
657
|
+
thread_pool << task
|
658
|
+
end
|
659
|
+
|
660
|
+
# Clears all timers, events and errors
|
661
|
+
def clear
|
662
|
+
thread_pool.clear
|
663
|
+
|
664
|
+
@errors.clear
|
665
|
+
@events.clear
|
666
|
+
@mutex.synchronize do
|
667
|
+
@every_cylce_events.clear
|
668
|
+
@timers.clear
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
# Clears all errors which occurred during the last step and are not marked as known
|
673
|
+
# If the errors were not cleared they are re raised the next time step is called.
|
674
|
+
def clear_errors
|
675
|
+
@errors.clear
|
676
|
+
end
|
677
|
+
|
678
|
+
def handle_error(error,save = true)
|
679
|
+
call do
|
680
|
+
on_error = @mutex.synchronize do
|
681
|
+
@on_error.find_all{|key,e| error.is_a? key}.map(&:last).flatten
|
682
|
+
end
|
683
|
+
on_error.each do |handler|
|
684
|
+
handler.call error
|
685
|
+
end
|
686
|
+
@errors << error if save == true
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
private
|
691
|
+
# Calls the given block and rescues all errors which can be handled
|
692
|
+
# by the added error handler. If an error cannot be handled it is
|
693
|
+
# stored and re raised after all events and timers are processed. If
|
694
|
+
# more than one error occurred which cannot be handled they are stored
|
695
|
+
# until the next step is called and re raised until all errors are processed.
|
696
|
+
#
|
697
|
+
# @info This method must be called from the event loop thread, otherwise
|
698
|
+
# all error handlers would be called from the wrong thread
|
699
|
+
#
|
700
|
+
# @yield The code block.
|
701
|
+
# @see #error_handler
|
702
|
+
def handle_errors(&block)
|
703
|
+
block.call
|
704
|
+
rescue Exception => e
|
705
|
+
handle_error(e,true)
|
706
|
+
end
|
707
|
+
|
708
|
+
public
|
709
|
+
# The EventLoop::Forwardable module provides delegation of specified methods to
|
710
|
+
# a designated object like the ruby ::Forwardable module but defers the
|
711
|
+
# method call to a thread pool of an event loop if a callback is given.
|
712
|
+
# After the call returned the callback is called from the event loop thread
|
713
|
+
# while it is processing its event at the end of each step.
|
714
|
+
#
|
715
|
+
# To ensure thread safety for all kind of objects the event loop defers
|
716
|
+
# only one method call per object in parallel even if the method is
|
717
|
+
# called without any callback. For this mechanism a sync key is used
|
718
|
+
# which is by default the designated object but can be set to any
|
719
|
+
# custom ruby object. If a method call is thread safe the sync key can
|
720
|
+
# be set to nil allowing the event loop to call it in parallel while
|
721
|
+
# another none thread safe method call of the designated object is processed.
|
722
|
+
#
|
723
|
+
# @note It is not possible to delegate methods where the target method needs
|
724
|
+
# a code block.
|
725
|
+
#
|
726
|
+
# @author Alexander Duda <Alexander.Duda@dfki.de>
|
727
|
+
module Forwardable
|
728
|
+
# Runtime error raised if the designated object is nil
|
729
|
+
# @author Alexander Duda <Alexander.Duda@dfki.de>
|
730
|
+
class DesignatedObjectNotFound < RuntimeError; end
|
731
|
+
|
732
|
+
# Defines a method as delegator instance method with an optional alias
|
733
|
+
# name ali.
|
734
|
+
#
|
735
|
+
# Method calls to ali will be delegated to accessor.method. If an error occurres
|
736
|
+
# during proccessing it will be raised like in the case of the original object but
|
737
|
+
# also forwarded to the error handlers of event loop.
|
738
|
+
#
|
739
|
+
# Method calls to ali(*args,&block) will be delegated to
|
740
|
+
# accessor.method(*args) but called from a thread pool. Thereby the code
|
741
|
+
# block is used as callback called from the main thread after the
|
742
|
+
# call returned. If an error occurred it will be:
|
743
|
+
# * given to the callback as second argument
|
744
|
+
# * forwarded to the error handlers of the event loop
|
745
|
+
# * raised at the beginning of the next step if not marked as known error
|
746
|
+
#
|
747
|
+
# To overwrite an error the callback can return :ignore_error or a
|
748
|
+
# new instance of an error. In an event of an error the error handlers of the
|
749
|
+
# event loop will not be called or called with the new error
|
750
|
+
# instance.
|
751
|
+
#
|
752
|
+
# ali do |result,exception|
|
753
|
+
# if exception
|
754
|
+
# MyError.new
|
755
|
+
# else
|
756
|
+
# puts result
|
757
|
+
# end
|
758
|
+
# end
|
759
|
+
#
|
760
|
+
# ali do |result,exception|
|
761
|
+
# if exception
|
762
|
+
# :ignore_error
|
763
|
+
# else
|
764
|
+
# puts result
|
765
|
+
# end
|
766
|
+
# end
|
767
|
+
#
|
768
|
+
# If the callback accepts only one argument
|
769
|
+
# the callback will not be called in an event of an error but
|
770
|
+
# the error will still be forwarded to the error handlers.
|
771
|
+
#
|
772
|
+
# If the result shall be filtered before returned a filter method can
|
773
|
+
# be specified which is called from the event loop thread just before
|
774
|
+
# the result is returned.
|
775
|
+
#
|
776
|
+
# @example
|
777
|
+
# class Dummy
|
778
|
+
# # non thread safe method
|
779
|
+
# def test(wait)
|
780
|
+
# sleep wait
|
781
|
+
# Thread.current
|
782
|
+
# end
|
783
|
+
#
|
784
|
+
# # thread safe method
|
785
|
+
# def test_thread_safe(wait)
|
786
|
+
# sleep wait
|
787
|
+
# Thread.current
|
788
|
+
# end
|
789
|
+
# end
|
790
|
+
# class DummyAsync
|
791
|
+
# extend Utilrb::EventLoop::Forwardable
|
792
|
+
# def_event_loop_delegator :@obj,:@event_loop,:test,:alias => :atest
|
793
|
+
# def_event_loop_delegator :@obj,:@event_loop,:test_thread_safe,:sync_key => false
|
794
|
+
#
|
795
|
+
# def initialize(event_loop)
|
796
|
+
# @event_loop = event_loop
|
797
|
+
# @obj = Dummy.new
|
798
|
+
# end
|
799
|
+
# end
|
800
|
+
#
|
801
|
+
# event_loop = EventLoop.new
|
802
|
+
# test = DummyAsync.new(event_loop)
|
803
|
+
# puts test.atest 2
|
804
|
+
# test.atest 2 do |result|
|
805
|
+
# puts result
|
806
|
+
# end
|
807
|
+
# test.thread_safe 2 do |result|
|
808
|
+
# puts result
|
809
|
+
# end
|
810
|
+
# sleep(0.1)
|
811
|
+
# event_loop.step
|
812
|
+
#
|
813
|
+
# @param [Symbol] accessor The symbol for the designated object.
|
814
|
+
# @param [Symbol] event_loop The event loop accessor.
|
815
|
+
# @param [Symbol] method The method called on the designated object.
|
816
|
+
# @param [Hash] options The options
|
817
|
+
# @option options [Symbol] :alias The alias of the method
|
818
|
+
# @option options [Symbol] :sync_key The sync key
|
819
|
+
# @option options [Symbol] :filter The filter method
|
820
|
+
# @option options [Symbol] :on_error Method which is called if an error occured
|
821
|
+
# @option options [class] :known_errors Known errors which will be rescued but still be forwarded.
|
822
|
+
# @see #sync
|
823
|
+
def def_event_loop_delegator(accessor,event_loop, method, options = Hash.new )
|
824
|
+
Forward.def_event_loop_delegator(self,accessor,event_loop,method,options)
|
825
|
+
end
|
826
|
+
|
827
|
+
def def_event_loop_delegators(accessor,event_loop, *methods)
|
828
|
+
Forward.def_event_loop_delegators(self,accessor,event_loop,*methods)
|
829
|
+
end
|
830
|
+
|
831
|
+
def forward_to(accessor,event_loop,options = Hash.new,&block)
|
832
|
+
obj = Forward.new(self,accessor,event_loop,options)
|
833
|
+
obj.instance_eval(&block)
|
834
|
+
end
|
835
|
+
|
836
|
+
private
|
837
|
+
class Forward
|
838
|
+
def initialize(klass,accessor,event_loop,options = Hash.new)
|
839
|
+
@klass = klass
|
840
|
+
@stack = [options]
|
841
|
+
@accessor = accessor
|
842
|
+
@event_loop = event_loop
|
843
|
+
end
|
844
|
+
|
845
|
+
def options(options = Hash.new,&block)
|
846
|
+
@stack << @stack.last.merge(options)
|
847
|
+
block.call
|
848
|
+
@stack.pop
|
849
|
+
end
|
850
|
+
|
851
|
+
def thread_safe(&block)
|
852
|
+
options :sync_key => nil do
|
853
|
+
block.call
|
854
|
+
end
|
855
|
+
end
|
856
|
+
|
857
|
+
def def_delegators(*methods)
|
858
|
+
options = if methods.last.is_a? Hash
|
859
|
+
methods.pop
|
860
|
+
else
|
861
|
+
Hash.new
|
862
|
+
end
|
863
|
+
methods << @stack.last.merge(options)
|
864
|
+
Forward.def_event_loop_delegators(@klass,@accessor,@event_loop,*methods)
|
865
|
+
end
|
866
|
+
|
867
|
+
def def_delegator(method,options = Hash.new)
|
868
|
+
options = @stack.last.merge options
|
869
|
+
Forward.def_event_loop_delegator(@klass,@accessor,@event_loop,method,options)
|
870
|
+
end
|
871
|
+
|
872
|
+
|
873
|
+
def self.def_event_loop_delegator(klass,accessor,event_loop, method, options = Hash.new )
|
874
|
+
options = Kernel.validate_options options, :filter => nil,
|
875
|
+
:alias => method,
|
876
|
+
:sync_key => :accessor,
|
877
|
+
:known_errors => nil,
|
878
|
+
:on_error => nil
|
879
|
+
|
880
|
+
raise ArgumentError, "accessor is nil" unless accessor
|
881
|
+
raise ArgumentError, "event_loop is nil" unless event_loop
|
882
|
+
raise ArgumentError, "method is nil" unless method
|
883
|
+
|
884
|
+
ali = options[:alias]
|
885
|
+
return if klass.instance_methods.include? ali.to_sym
|
886
|
+
|
887
|
+
filter = options[:filter]
|
888
|
+
sync_key = options[:sync_key]
|
889
|
+
sync_key ||= :nil
|
890
|
+
errors = "[#{Array(options[:known_errors]).map(&:name).join(",")}]"
|
891
|
+
on_error = options[:on_error]
|
892
|
+
|
893
|
+
line_no = __LINE__; str = %Q{
|
894
|
+
def #{ali}(*args, &block)
|
895
|
+
options = Hash.new
|
896
|
+
accessor,error = #{if options[:known_errors]
|
897
|
+
%Q{
|
898
|
+
begin
|
899
|
+
#{accessor} # cache the accessor.
|
900
|
+
rescue #{Array(options[:known_errors]).join(",")} => e
|
901
|
+
[nil,e]
|
902
|
+
end
|
903
|
+
}
|
904
|
+
else
|
905
|
+
accessor.to_s
|
906
|
+
end}
|
907
|
+
if !block
|
908
|
+
begin
|
909
|
+
if !accessor
|
910
|
+
error ||= DesignatedObjectNotFound.new 'designated object is nil'
|
911
|
+
raise error
|
912
|
+
else
|
913
|
+
result = #{sync_key != :nil ? "#{event_loop}.sync(#{sync_key}){accessor.__send__(:#{method}, *args)}" : "accessor.__send__(:#{method}, *args)"}
|
914
|
+
#{filter ? "#{filter}(result)" : "result"}
|
915
|
+
end
|
916
|
+
rescue Exception => error
|
917
|
+
#{"#{on_error}(error)" if on_error}
|
918
|
+
raise error
|
919
|
+
end
|
920
|
+
else
|
921
|
+
work = Proc.new do |*args|
|
922
|
+
acc,err = #{accessor} # cache accessor
|
923
|
+
if !acc
|
924
|
+
if err
|
925
|
+
raise err
|
926
|
+
else
|
927
|
+
raise DesignatedObjectNotFound,'designated object is nil'
|
928
|
+
end
|
929
|
+
else
|
930
|
+
acc.__send__(:#{method}, *args, &block)
|
931
|
+
end
|
932
|
+
end
|
933
|
+
callback = #{filter ? "block.to_proc.arity == 2 ? Proc.new { |r,e| block.call(#{filter}(r),e)} : Proc.new {|r| block.call(#{filter}(r))}" : "block"}
|
934
|
+
#{event_loop}.async_with_options(work,
|
935
|
+
{:sync_key => #{sync_key},:known_errors => #{errors},
|
936
|
+
:on_error => #{ on_error ? "self.method(#{on_error.inspect})" : "nil" }},
|
937
|
+
*args, &callback)
|
938
|
+
end
|
939
|
+
rescue Exception
|
940
|
+
$@.delete_if{|s| %r"#{Regexp.quote(__FILE__)}"o =~ s}
|
941
|
+
::Kernel::raise
|
942
|
+
end
|
943
|
+
}
|
944
|
+
# If it's not a class or module, it's an instance
|
945
|
+
begin
|
946
|
+
klass.module_eval(str, __FILE__, line_no)
|
947
|
+
rescue
|
948
|
+
klass.instance_eval(str, __FILE__, line_no)
|
949
|
+
end
|
950
|
+
end
|
951
|
+
|
952
|
+
# Defines multiple method as delegator instance methods
|
953
|
+
# @see #def_event_loop_delegator
|
954
|
+
def self.def_event_loop_delegators(klass,accessor,event_loop, *methods)
|
955
|
+
methods.flatten!
|
956
|
+
options = if methods.last.is_a? Hash
|
957
|
+
methods.pop
|
958
|
+
else
|
959
|
+
Hash.new
|
960
|
+
end
|
961
|
+
raise ArgumentError, ":alias is not supported when defining multiple methods at once." if options.has_key?(:alias)
|
962
|
+
methods.each do |method|
|
963
|
+
def_event_loop_delegator(klass,accessor,event_loop,method,options)
|
964
|
+
end
|
965
|
+
end
|
966
|
+
end
|
967
|
+
end
|
968
|
+
end
|
969
|
+
end
|