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.
@@ -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