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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +1 -0
- data.tar.gz.sig +0 -0
- data/.appveyor.yml +28 -0
- data/.gitignore +8 -0
- data/.travis.yml +16 -0
- data/.yardopts +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +381 -0
- data/Rakefile +14 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/downloads.md +143 -0
- data/docs/server.md +88 -0
- data/docs/threadpool.md +73 -0
- data/eventbox.gemspec +31 -0
- data/lib/eventbox.rb +270 -0
- data/lib/eventbox/argument_wrapper.rb +76 -0
- data/lib/eventbox/boxable.rb +298 -0
- data/lib/eventbox/event_loop.rb +385 -0
- data/lib/eventbox/object_registry.rb +35 -0
- data/lib/eventbox/sanitizer.rb +342 -0
- data/lib/eventbox/thread_pool.rb +170 -0
- data/lib/eventbox/timer.rb +172 -0
- data/lib/eventbox/version.rb +5 -0
- metadata +156 -0
- metadata.gz.sig +0 -0
data/Rakefile
ADDED
@@ -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
|
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/docs/downloads.md
ADDED
@@ -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.
|
data/docs/server.md
ADDED
@@ -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
|
+
```
|
data/docs/threadpool.md
ADDED
@@ -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.
|
data/eventbox.gemspec
ADDED
@@ -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
|
data/lib/eventbox.rb
ADDED
@@ -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
|