eventbox 0.1.0

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,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