eventbox 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "yard"
4
+
5
+ Rake::TestTask.new(:test) do |t|
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ YARD::Rake::YardocTask.new do |t|
12
+ end
13
+
14
+ task :gem => :build
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "eventbox"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,143 @@
1
+ ### Use Eventbox to download URLs concurrently
2
+
3
+ The following example illustrates how to use actions in order to download a list of URLs in parallel.
4
+
5
+ At first the `init` method starts an action for each URL to be downloaded, initializes some variables and stores the `result` object for later use.
6
+ Since the `result` is not yielded in the method body, the external call to `ParallelDownloads.new` doesn't return to that point in time.
7
+ Instead it's suspended until `result` is yielded later on, when all URLs have been retrieved.
8
+
9
+ ### Running actions
10
+
11
+ Each call to the action method `start_download` starts a new thread (or at least borrows one from the thread-pool).
12
+ That way we leave the protected event scope of {Eventbox.async_call async_call}, {Eventbox.sync_call sync_call} and {Eventbox.yield_call yield_call} methods and enter the action scope which runs concurrently.
13
+ Since actions don't have access to instance variables, all required information must be passed as method arguments.
14
+ This is intentionally, because all arguments pass the {Eventbox::Sanitizer} that way, which protects from data races and translates between internal event based and external blocking behavior of `Proc` objects.
15
+ Actions should never use shared data directly or share any data with other program parts, but should use event scope methods like {Eventbox.sync_call sync_call} or closures like {Eventbox#yield_proc yield_proc} to access shared data in a thread-safe way.
16
+
17
+ ### Catching errors
18
+
19
+ Another typical and recommended code sequence is the `rescue` / `else` declaration in an action method.
20
+ They inform the Eventbox object about success or failure of a particular action.
21
+ This outcome can then be properly handled by event scope methods.
22
+ In our case either the received data or the received exception is sent to `download_finished`.
23
+ It is a event scope method, so that it can safely access instance variables.
24
+ If all downloads completed, the result object received at `init` is yielded, so that the external call to `ParallelDownloads.new` returns.
25
+
26
+ Let's see how this looks in practice:
27
+
28
+ ```ruby
29
+ require "eventbox"
30
+ require "net/https"
31
+ require "open-uri"
32
+ require "pp"
33
+
34
+ # Build a new Eventbox based class, which makes use of a pool of two threads.
35
+ # This way the number of concurrent downloads is limited to 3.
36
+ class ParallelDownloads < Eventbox.with_options(threadpool: Eventbox::ThreadPool.new(3))
37
+
38
+ # Called at ParallelDownloads.new just like Object#initialize in ordinary ruby classes
39
+ # Yield calls get one additional argument and suspend the caller until result.yield is invoked
40
+ yield_call def init(urls, result, &progress)
41
+ @urls = urls
42
+ @urls.each do |url| # Start a download thread for each URL
43
+ start_download(url) # Start the download - the call returns immediately
44
+ end
45
+ # It's safe to set instance variables after start_download
46
+ @downloads = {} # The result hash with all downloads
47
+ @finished = result # Don't return to the caller, but store result yielder for later
48
+ @progress = progress
49
+ end
50
+
51
+ # Each call to an action method starts a new thread
52
+ # Actions don't have access to instance variables.
53
+ private action def start_download(url)
54
+ data = OpenURI.open_uri(url) # HTTP GET url
55
+ .read(100).each_line.first # Retrieve the first line but max 100 bytes
56
+ rescue SocketError => err # Catch any network errors
57
+ download_finished(url, err) # and store it in the result hash
58
+ else
59
+ download_finished(url, data) # ... or store the retrieved data when successful
60
+ end
61
+
62
+ # Called for each finished download
63
+ private sync_call def download_finished(url, res)
64
+ @downloads[url] = res # Store the download result in the result hash
65
+ @progress&.yield(@downloads.size) # Notify the caller about our progress
66
+ if @downloads.size == @urls.size # All downloads finished?
67
+ @finished.yield # Finish ParallelDownloads.new
68
+ end
69
+ end
70
+
71
+ attr_reader :downloads # Threadsafe access to @download
72
+ end
73
+
74
+ urls = %w[
75
+ http://ruby-lang.org
76
+ http://ruby-lang.ooorg
77
+ http://wikipedia.org
78
+ http://torproject.org
79
+ http://github.com
80
+ ]
81
+
82
+ d = ParallelDownloads.new(urls) { |progress| print progress }
83
+ pp d.downloads
84
+ ```
85
+
86
+ This prints the numbers 1 to 5 as downloads finish and subsequently prints the reveived HTML text, so that the output looks like the following.
87
+ The order depends on the particular response time of the URL.
88
+
89
+ ```ruby
90
+ 12345{"http://ruby-lang.ooorg"=>#<SocketError: Failed to open TCP connection to ruby-lang.ooorg:80 (getaddrinfo: Name or service not known)>,
91
+ "http://wikipedia.org"=>"<!DOCTYPE html>\n",
92
+ "http://torproject.org"=>"<div class=\"eoy-background\">\n",
93
+ "http://ruby-lang.org"=>"<!DOCTYPE html>\n",
94
+ "http://github.com"=>"\n"}
95
+ ```
96
+
97
+ Since Eventbox protects from data races, it's insignificant in which order events are emitted by an event scope method and whether objects are changed after being sent.
98
+ It's therefore OK to set `@downloads` both before or after starting the action threads per `start_download` in `init`.
99
+
100
+ ### Change to closure style
101
+
102
+ There is another alternative way to transmit the result of an action to the event scope.
103
+ Instead of calling a {Eventbox.sync_call sync_call} method a closure like {Eventbox.sync_proc sync_proc} can be used.
104
+ It is simply the anonymous form of {Eventbox.sync_call sync_call}.
105
+ It behaves exactly identical, but is passed as argument.
106
+ This means in particular, that it's thread-safe to call {Eventbox.sync_proc sync_proc} from an action or external scope.
107
+
108
+ The above class rewritten to the closure style looks like so:
109
+
110
+ ```ruby
111
+ class ParallelDownloads < Eventbox.with_options(threadpool: Eventbox::ThreadPool.new(3))
112
+
113
+ yield_call def init(urls, result, &progress)
114
+ urls.each do |url| # Start a download thread for each URL
115
+
116
+ on_finished = sync_proc do |res| # Create a closure object comparable to sync_call
117
+ @downloads[url] = res # Store the download result in the result hash
118
+ progress&.yield(@downloads.size) # Notify the caller about our progress
119
+ if @downloads.size == urls.size # All downloads finished?
120
+ result.yield # Let ParallelDownloads.new return
121
+ end
122
+ end
123
+
124
+ start_download(url, on_finished) # Start the download - the call returns immediately
125
+ end
126
+ @downloads = {} # The result hash with all downloads
127
+ end
128
+
129
+ private action def start_download(url, on_finished)
130
+ data = OpenURI.open_uri(url) # HTTP GET url
131
+ .read(100).each_line.first # Retrieve the first line but max 100 bytes
132
+ rescue SocketError => err # Catch any network errors
133
+ on_finished.yield(err) # and store it in the result hash
134
+ else
135
+ on_finished.yield(data) # ... or store the retrieved data when successful
136
+ end
137
+
138
+ attr_reader :downloads # Threadsafe access to @download
139
+ end
140
+ ```
141
+
142
+ I guess that friends of object orientated programming probably like the method style more, while fans of functional programming prefer closures.
143
+ All in all it's purely a matter of taste whether you prefer the method or the closure style.
@@ -0,0 +1,88 @@
1
+ Race-free server startup and shutdown can be a tricky task.
2
+ The following example illustrates, how a TCP server can be started and interrupted properly.
3
+
4
+ ```ruby
5
+ require "eventbox"
6
+ require "socket"
7
+
8
+ class MyServer < Eventbox
9
+ yield_call def init(bind, port, result)
10
+ @count = 0
11
+ @server = start_serving(bind, port, result)
12
+ end
13
+
14
+ action def start_serving(bind, port, init_done)
15
+ serv = TCPServer.new(bind, port)
16
+ rescue => err
17
+ init_done.raise err
18
+ else
19
+ init_done.yield
20
+
21
+ loop do
22
+ begin
23
+ conn = Thread.handle_interrupt(Stop => :on_blocking) do
24
+ serv.accept
25
+ end
26
+ rescue Stop => st
27
+ serv.close
28
+ st.stopped.yield
29
+ break
30
+ else
31
+ MyConnection.new(conn, self)
32
+ end
33
+ end
34
+ end
35
+
36
+ sync_call def count
37
+ @count += 1
38
+ end
39
+
40
+ yield_call def stop(result)
41
+ @server.raise(Stop.new(result))
42
+ end
43
+
44
+ class Stop < RuntimeError
45
+ def initialize(stopped)
46
+ @stopped = stopped
47
+ end
48
+ attr_reader :stopped
49
+ end
50
+ end
51
+
52
+ class MyConnection < Eventbox
53
+ action def init(conn, server)
54
+ conn.write "Hello #{server.count}"
55
+ ensure
56
+ conn.close
57
+ end
58
+ end
59
+ ```
60
+
61
+ The server can now be started like so.
62
+
63
+ ```ruby
64
+ s = MyServer.new('localhost', 12345)
65
+
66
+ 10.times.map do
67
+ Thread.new do
68
+ TCPSocket.new('localhost', 12345).read
69
+ end
70
+ end.each { |th| p th.value }
71
+
72
+ s.stop
73
+ ```
74
+
75
+ It prints some output like this:
76
+
77
+ ```ruby
78
+ "Hello 2"
79
+ "Hello 1"
80
+ "Hello 7"
81
+ "Hello 8"
82
+ "Hello 3"
83
+ "Hello 9"
84
+ "Hello 5"
85
+ "Hello 6"
86
+ "Hello 4"
87
+ "Hello 10"
88
+ ```
@@ -0,0 +1,73 @@
1
+ The following class implements a thread-pool with a fixed number of threads to be borrowed by the `pool` method.
2
+ It shows how the action method `start_pool_thread` makes use of the private yield_call `next_job` to query, wait for and retrieve an object from the event scope.
3
+
4
+ This kind of object is the block that is given to `pool`.
5
+ Although all closures (blocks, procs and lambdas) are wrapped in a way that allows safe calls from the event scope, it is just passed through to the action scope and retrieved as the result value of `next_job`.
6
+ When this happens, the wrapping is automatically removed, so that the pure block given to `pool` is called in `start_pool_thread`.
7
+
8
+ ```ruby
9
+ class ThreadPool < Eventbox
10
+ async_call def init(pool_size)
11
+ @que = [] # Initialize an empty job queue
12
+ @jobless = [] # Initialize the list of jobless action threads
13
+
14
+ pool_size.times do # Start up x action threads
15
+ start_pool_thread
16
+ end
17
+ end
18
+
19
+ # The action call returns immediately, but spawns a new thread.
20
+ private action def start_pool_thread
21
+ while bl=next_job # Each new thread waits for a job to be pooled
22
+ bl.call # Execute the external job enqueued by `pool`
23
+ end
24
+ end
25
+
26
+ # Get the next job or wait for one
27
+ # The method is private, so that it's accessible in start_pool_thread action but not externally
28
+ private yield_call def next_job(result)
29
+ if @que.empty? # No job pooled?
30
+ @jobless << result # Enqueue the action thread to the list of jobless workers
31
+ else # Already pooled jobs?
32
+ result.yield @que.shift # Take the oldest job and let next_job return with this job
33
+ end
34
+ end
35
+
36
+ # Enqueue a new job
37
+ async_call def pool(&block)
38
+ if @jobless.empty? # No jobless thread available?
39
+ @que << block # Append the external block as job into the queue
40
+ else # A thread is waiting?
41
+ @jobless.shift.yield block # Take one thread and let next_job return the given job
42
+ end # so that it is processed by the pool_thread action above
43
+ end
44
+ end
45
+ ```
46
+
47
+ This `ThreadPool` can be used like so:
48
+
49
+ ```ruby
50
+ tp = ThreadPool.new(3) # Create a thread pool with 3 action threads
51
+ 5.times do |i| # Start 5 jobs concurrently
52
+ tp.pool do # pool never blocks, but enqueues jobs when no free thread is available
53
+ sleep 1 # The mission of each job: Wait for 1 second (3 jobs concurrently)
54
+ p [i, Thread.current.object_id]
55
+ end
56
+ end
57
+
58
+ # It gives something like the following output after 1 second:
59
+ [2, 47030774465880]
60
+ [1, 47030775602740]
61
+ [0, 47030774464940]
62
+ # and something like this after one more seconds:
63
+ [3, 47030775602740]
64
+ [4, 47030774465880]
65
+ ```
66
+
67
+ Eventbox's builtin thread-pool {Eventbox::ThreadPool} is implemented on top of Eventbox similar to the above.
68
+ In addition there are various battle proof implementations of thread-pools such a these in [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby), which are faster and more feature rich than the above.
69
+
70
+ However Eventbox comes into play when things are getting more complicated or more customized.
71
+ Imagine the thread-pool has to schedule it's tasks not just to cheep threads, but to more expensive or more constraint resources.
72
+ In such cases available abstractions don't fit well to the problem.
73
+ Instead the above example can be used as a basis for your own extensions.
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "eventbox/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "eventbox"
7
+ spec.version = Eventbox::VERSION
8
+ spec.authors = ["Lars Kanis"]
9
+ spec.email = ["lars@greiz-reinsdorf.de"]
10
+
11
+ if File.read("README.md", encoding: 'utf-8') =~ /^_(.*?)_$\s^\n(.*?)\n$/m
12
+ spec.summary = $1
13
+ spec.description = $2
14
+ end
15
+ spec.homepage = "https://github.com/larskanis/eventbox"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test)/}) }
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+ spec.required_ruby_version = "~> 2.3"
25
+ spec.metadata["yard.run"] = "yri" # use "yard" to build full HTML docs.
26
+
27
+ spec.add_development_dependency "bundler", ">= 1.16", "< 3"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency "minitest", "~> 5.0"
30
+ spec.add_development_dependency "yard", "~> 0.9"
31
+ end
@@ -0,0 +1,270 @@
1
+ # frozen-string-literal: true
2
+
3
+ require "weakref"
4
+ require "eventbox/argument_wrapper"
5
+ require "eventbox/sanitizer"
6
+ require "eventbox/boxable"
7
+ require "eventbox/event_loop"
8
+ require "eventbox/object_registry"
9
+
10
+ class Eventbox
11
+ autoload :VERSION, "eventbox/version"
12
+ autoload :ThreadPool, "eventbox/thread_pool"
13
+ autoload :Timer, "eventbox/timer"
14
+
15
+ extend Boxable
16
+
17
+ class InvalidAccess < RuntimeError; end
18
+ class MultipleResults < RuntimeError; end
19
+ class AbortAction < RuntimeError; end
20
+
21
+ if RUBY_ENGINE=='jruby' && RUBY_VERSION.split(".").map(&:to_i).pack("C*") < [9,2,1,0].pack("C*") ||
22
+ RUBY_ENGINE=='truffleruby'
23
+ # This is a workaround for bug https://github.com/jruby/jruby/issues/5314
24
+ # which was fixed in JRuby-9.2.1.0.
25
+ class Thread < ::Thread
26
+ def initialize(*args, &block)
27
+ started = Queue.new
28
+ super do
29
+ Thread.handle_interrupt(Exception => :never) do
30
+ started << true
31
+ block.call(*args)
32
+ # Immediately stop the thread, before the handle_interrupt has finished.
33
+ # This is necessary for JRuby to avoid possoble signal handling after the block.
34
+ Thread.exit
35
+ end
36
+ end
37
+ started.pop
38
+ end
39
+ end
40
+ end
41
+
42
+ # Retrieves the Eventbox options of this class.
43
+ #
44
+ # @return [Hash] The options for instantiation of this class.
45
+ # @see with_options
46
+ def self.eventbox_options
47
+ {
48
+ threadpool: Thread,
49
+ guard_time: 0.5,
50
+ gc_actions: false,
51
+ }
52
+ end
53
+
54
+ # Create a new derived class with the given options.
55
+ #
56
+ # The options are merged with the options of the base class.
57
+ # The following options are available:
58
+ #
59
+ # @param threadpool [Object] A threadpool.
60
+ # Can be either +Thread+ (default) or a {Eventbox::Threadpool} instance.
61
+ # @param guard_time Event scope methods should not do blocking operations.
62
+ # Eventbox measures the time of each call to event scope methods and warns, when it is exceeded.
63
+ # There are several ways to configure guard_time:
64
+ # * Set to +nil+: Disable measuring of time to process event scope methods.
65
+ # * Set to a +Numeric+ value: Maximum number of seconds allowed for event scope methods.
66
+ # * Set to a +Proc+ object: Called after each call to an event scope method.
67
+ # The +Proc+ object is called with the number of seconds the call took as first and the name as second argument.
68
+ # @param gc_actions [Boolean] Enable or disable (default) garbage collection of running actions.
69
+ # Setting this to true permits the garbage collector to shutdown running action threads and subsequently delete the corresponding Eventbox object.
70
+ def self.with_options(**options)
71
+ Class.new(self) do
72
+ define_singleton_method(:eventbox_options) do
73
+ super().merge(options)
74
+ end
75
+
76
+ def self.inspect
77
+ klazz = self
78
+ until name=klazz.name
79
+ klazz = klazz.superclass
80
+ end
81
+ "#{name}#{eventbox_options}"
82
+ end
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # @private
89
+ #
90
+ # Create a new {Eventbox} instance.
91
+ #
92
+ # All arguments are passed to the init() method when defined.
93
+ def initialize(*args, &block)
94
+ options = self.class.eventbox_options
95
+
96
+ # This instance variable is set to self here, but replaced by Boxable#action to a WeakRef
97
+ @__eventbox__ = self
98
+
99
+ # Verify that all public methods are properly wrapped and no unsafe methods exist
100
+ # This check is done at the first instanciation only and doesn't slow down subsequently.
101
+ # Since test and set operations aren't atomic, it can happen that the check is executed several times.
102
+ # This is considered less harmful than slowing all instanciations down by a mutex.
103
+ unless self.class.instance_variable_defined?(:@eventbox_methods_checked)
104
+ self.class.instance_variable_set(:@eventbox_methods_checked, true)
105
+
106
+ obj = Object.new
107
+ meths = methods - obj.methods - [:__getobj__, :shutdown!, :shared_object]
108
+ prmeths = private_methods - obj.private_methods
109
+ prohib = meths.find do |name|
110
+ !prmeths.include?(:"__#{name}__")
111
+ end
112
+ if prohib
113
+ meth = method(prohib)
114
+ raise InvalidAccess, "method `#{prohib}' at #{meth.source_location.join(":")} is not properly defined -> it must be created per async_call, sync_call, yield_call or private prefix"
115
+ end
116
+ end
117
+
118
+ # Run the processing of calls (the event loop) in a separate class.
119
+ # Otherwise it would block GC'ing of self.
120
+ @__event_loop__ = EventLoop.new(options[:threadpool], options[:guard_time])
121
+ ObjectSpace.define_finalizer(self, @__event_loop__.method(:send_shutdown))
122
+
123
+ init(*args, &block)
124
+ end
125
+
126
+ def self.method_added(name)
127
+ if name==:initialize
128
+ meth = instance_method(:initialize)
129
+ raise InvalidAccess, "method `initialize' at #{meth.source_location.join(":")} must not be overwritten - use `init' instead"
130
+ end
131
+ end
132
+
133
+ # @private
134
+ #
135
+ # Provide access to the eventbox instance as either
136
+ # - self within the eventbox instance itself or
137
+ # - WeakRef.new(self).__getobj__ within actions.
138
+ # This allows actions to be GC'ed, when the related Eventbox instance is no longer in use.
139
+ def eventbox
140
+ @__eventbox__.__getobj__
141
+ end
142
+
143
+ # @private
144
+ protected def __getobj__
145
+ self
146
+ end
147
+
148
+ private
149
+
150
+ # Initialize a new {Eventbox} instance.
151
+ #
152
+ # This method is executed for initialization of a Eventbox instance.
153
+ # This method receives all arguments given to +Eventbox.new+ after they have passed the {Sanitizer}.
154
+ # It can be used like +initialize+ in ordinary ruby classes including +super+ to initialize included modules or base classes.
155
+ #
156
+ # {init} can be defined as either {sync_call} or {async_call} with no difference.
157
+ # {init} can also be defined as {yield_call}, so that the +new+ call is blocked until the result is yielded.
158
+ # {init} can even be defined as {action}, so that each instance of the class immediately starts a new thread.
159
+ def init(*args)
160
+ end
161
+
162
+ # Create a proc object for asynchronous (fire-and-forget) calls similar to {async_call}.
163
+ #
164
+ # It can be passed to external scope and called from there like so:
165
+ #
166
+ # class MyBox < Eventbox
167
+ # sync_call def print(p1)
168
+ # async_proc do |p2|
169
+ # puts "#{p1} #{p2}"
170
+ # end
171
+ # end
172
+ # end
173
+ # MyBox.new.print("Hello").call("world") # Prints "Hello world"
174
+ #
175
+ # The created object can be safely called from any thread.
176
+ # All block arguments are passed through the {Sanitizer}.
177
+ # The block itself might not do any blocking calls or expensive computations - this would impair responsiveness of the {Eventbox} instance.
178
+ # Instead use {Eventbox.action} in these cases.
179
+ #
180
+ # The block always returns +self+ to the caller.
181
+ def async_proc(name=nil, &block)
182
+ @__event_loop__.new_async_proc(name=nil, &block)
183
+ end
184
+
185
+ # Create a Proc object for synchronous calls similar to {sync_call}.
186
+ #
187
+ # It can be passed to external scope and called from there like so:
188
+ #
189
+ # class MyBox < Eventbox
190
+ # sync_call def print(p1)
191
+ # sync_proc do |p2|
192
+ # "#{p1} #{p2}"
193
+ # end
194
+ # end
195
+ # end
196
+ # puts MyBox.new.print("Hello").call("world") # Prints "Hello world"
197
+ #
198
+ # The created object can be safely called from any thread.
199
+ # All block arguments as well as the result value are passed through the {Sanitizer}.
200
+ # The block itself might not do any blocking calls or expensive computations - this would impair responsiveness of the {Eventbox} instance.
201
+ # Instead use {Eventbox.action} in these cases.
202
+ #
203
+ # This Proc is simular to {async_proc}, but when the block is invoked, it is executed and it's return value is returned to the caller.
204
+ # Since all processing within the event scope of an {Eventbox} instance must not execute blocking operations, sync procs can only return immediate values.
205
+ # For deferred results use {yield_proc} instead.
206
+ def sync_proc(name=nil, &block)
207
+ @__event_loop__.new_sync_proc(name=nil, &block)
208
+ end
209
+
210
+ # Create a Proc object for calls with deferred result similar to {yield_call}.
211
+ #
212
+ # It can be passed to external scope and called from there like so:
213
+ #
214
+ # class MyBox < Eventbox
215
+ # sync_call def print(p1)
216
+ # yield_proc do |p2, result|
217
+ # result.yield "#{p1} #{p2}"
218
+ # end
219
+ # end
220
+ # end
221
+ # puts MyBox.new.print("Hello").call("world") # Prints "Hello world"
222
+ #
223
+ # This proc type is simular to {sync_proc}, however it's not the result of the block that is returned.
224
+ # Instead the block is called with one additional argument in the event scope, which is used to yield a result value.
225
+ # The result value can be yielded within the called block, but it can also be called by any other event scope or external method, leading to a deferred proc return.
226
+ # The external thread calling this proc is suspended until a result is yielded.
227
+ # However the Eventbox object keeps responsive to calls from other threads.
228
+ #
229
+ # The created object can be safely called from any thread.
230
+ # If yield procs are called in the event scope, they must get a Proc object as the last argument.
231
+ # It is called when a result was yielded.
232
+ #
233
+ # All block arguments as well as the result value are passed through the {Sanitizer}.
234
+ # The block itself might not do any blocking calls or expensive computations - this would impair responsiveness of the {Eventbox} instance.
235
+ # Instead use {Eventbox.action} in these cases.
236
+ def yield_proc(name=nil, &block)
237
+ @__event_loop__.new_yield_proc(name=nil, &block)
238
+ end
239
+
240
+ # Mark an object as to be shared instead of copied.
241
+ #
242
+ # A marked object is never passed as copy, but passed as reference.
243
+ # The object is therefore wrapped as {WrappedObject} when used in an unsafe scope.
244
+ # Wrapping as {WrappedObject} denies access from external/action scope to event scope objects and vice versa.
245
+ # It also denies access to objects originated from a foreign event scope.
246
+ # However the object can be passed as reference and is automatically unwrapped when passed back to the original scope.
247
+ # It can therefore be used to modify the original object even after traversing the boundaries.
248
+ #
249
+ # Wrapping and unwrapping works even if the shared object is stored within another object as instance variable or within a collection class.
250
+ #
251
+ # The mark is stored for the lifetime of the object, so that it's enough to mark only once at object creation.
252
+ public def shared_object(object)
253
+ @__event_loop__.shared_object(object)
254
+ end
255
+
256
+ # Force stop of all action threads spawned by this {Eventbox} instance.
257
+ #
258
+ # It is possible to enable automatic cleanup of action threads by the garbage collector through {Eventbox.with_options}.
259
+ # However in some cases automatic garbage collection doesn't remove all instances due to running action threads.
260
+ # Calling shutdown! when the work of the instance is done, ensures that it is GC'ed in all cases.
261
+ #
262
+ # If {shutdown!} is called externally, it blocks until all actions threads have terminated.
263
+ #
264
+ # If {shutdown!} is called in the event scope, it just triggers the termination of all action threads and returns afterwards.
265
+ # Optionally {shutdown!} can be called with a block.
266
+ # It is called when all actions threads terminated.
267
+ public def shutdown!(&completion_block)
268
+ @__event_loop__.shutdown(&completion_block)
269
+ end
270
+ end