utilrb 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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