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