eventbox 0.1.0 → 1.0.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.appveyor.yml +5 -3
- data/.travis.yml +4 -4
- data/.yardopts +2 -2
- data/CHANGELOG.md +19 -0
- data/README.md +91 -48
- data/docs/downloads.md +6 -5
- data/docs/images/my_queue_calls.svg +761 -0
- data/docs/my_queue_calls_github.md +1 -0
- data/docs/my_queue_calls_local.md +1 -0
- data/docs/server.md +25 -14
- data/docs/threadpool.md +4 -1
- data/eventbox.gemspec +3 -2
- data/lib/eventbox.rb +63 -11
- data/lib/eventbox/argument_wrapper.rb +11 -10
- data/lib/eventbox/boxable.rb +41 -33
- data/lib/eventbox/call_context.rb +47 -0
- data/lib/eventbox/event_loop.rb +167 -70
- data/lib/eventbox/sanitizer.rb +155 -39
- data/lib/eventbox/thread_pool.rb +10 -0
- data/lib/eventbox/timer.rb +17 -7
- data/lib/eventbox/version.rb +1 -1
- metadata +50 -30
- metadata.gz.sig +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
[](https://www.rubydoc.info/github/larskanis/eventbox/master/file/README.md#my_queue_image)
|
@@ -0,0 +1 @@
|
|
1
|
+
{include:file:docs/images/my_queue_calls.svg}
|
data/docs/server.md
CHANGED
@@ -1,6 +1,13 @@
|
|
1
|
+
## A TCP server implementation with tracking of startup and shutdown
|
2
|
+
|
1
3
|
Race-free server startup and shutdown can be a tricky task.
|
2
4
|
The following example illustrates, how a TCP server can be started and interrupted properly.
|
3
5
|
|
6
|
+
For startup it makes use of {Eventbox::CompletionProc yield} and {Eventbox::CompletionProc#raise} to complete `MyServer.new` either successfully or with the forwarded exception raised by `TCPServer.new`.
|
7
|
+
|
8
|
+
For the shutdown it makes use of {Eventbox::Action#raise} to send a `Stop` signal to the blocking `accept` method.
|
9
|
+
The `Stop` instance carries the {Eventbox::CompletionProc} which is used to signal that the shutdown has finished by returning from `MyServer#stop`.
|
10
|
+
|
4
11
|
```ruby
|
5
12
|
require "eventbox"
|
6
13
|
require "socket"
|
@@ -8,36 +15,39 @@ require "socket"
|
|
8
15
|
class MyServer < Eventbox
|
9
16
|
yield_call def init(bind, port, result)
|
10
17
|
@count = 0
|
11
|
-
@server = start_serving(bind, port, result)
|
18
|
+
@server = start_serving(bind, port, result) # Start an action to handle incomming connections
|
12
19
|
end
|
13
20
|
|
14
21
|
action def start_serving(bind, port, init_done)
|
15
22
|
serv = TCPServer.new(bind, port)
|
16
23
|
rescue => err
|
17
|
-
init_done.raise err
|
24
|
+
init_done.raise err # complete MyServer.new with an exception
|
18
25
|
else
|
19
|
-
init_done.yield
|
26
|
+
init_done.yield # complete MyServer.new without exception
|
20
27
|
|
21
|
-
loop do
|
28
|
+
loop do # accept all connection requests until Stop is received
|
22
29
|
begin
|
30
|
+
# enable interruption by the Stop class for the duration of the `accept` call
|
23
31
|
conn = Thread.handle_interrupt(Stop => :on_blocking) do
|
24
|
-
serv.accept
|
32
|
+
serv.accept # wait for the next connection request come in
|
25
33
|
end
|
26
34
|
rescue Stop => st
|
27
35
|
serv.close
|
28
|
-
st.stopped.yield
|
29
|
-
break
|
36
|
+
st.stopped.yield # let MyServer#stop return
|
37
|
+
break # and exit the action
|
30
38
|
else
|
31
|
-
MyConnection.new(conn, self)
|
39
|
+
MyConnection.new(conn, self) # Handle each client by its own instance
|
32
40
|
end
|
33
41
|
end
|
34
42
|
end
|
35
43
|
|
44
|
+
# A simple example for a shared resource to be used by several threads
|
36
45
|
sync_call def count
|
37
|
-
@count += 1
|
46
|
+
@count += 1 # atomically increment the counter
|
38
47
|
end
|
39
48
|
|
40
49
|
yield_call def stop(result)
|
50
|
+
# Don't return from `stop` externally, but wait until the server is down
|
41
51
|
@server.raise(Stop.new(result))
|
42
52
|
end
|
43
53
|
|
@@ -49,11 +59,12 @@ class MyServer < Eventbox
|
|
49
59
|
end
|
50
60
|
end
|
51
61
|
|
62
|
+
# Each call to `MyConnection.new` starts a new thread to do the communication.
|
52
63
|
class MyConnection < Eventbox
|
53
64
|
action def init(conn, server)
|
54
65
|
conn.write "Hello #{server.count}"
|
55
66
|
ensure
|
56
|
-
conn.close
|
67
|
+
conn.close # Don't wait for an answer but just close the client connection
|
57
68
|
end
|
58
69
|
end
|
59
70
|
```
|
@@ -61,15 +72,15 @@ end
|
|
61
72
|
The server can now be started like so.
|
62
73
|
|
63
74
|
```ruby
|
64
|
-
s = MyServer.new('localhost', 12345)
|
75
|
+
s = MyServer.new('localhost', 12345) # Open a TCP socket
|
65
76
|
|
66
|
-
10.times.map do
|
77
|
+
10.times.map do # run 10 client connections in parallel
|
67
78
|
Thread.new do
|
68
79
|
TCPSocket.new('localhost', 12345).read
|
69
80
|
end
|
70
|
-
end.each { |th| p th.value }
|
81
|
+
end.each { |th| p th.value } # and print their responses
|
71
82
|
|
72
|
-
s.stop
|
83
|
+
s.stop # shutdown the server socket
|
73
84
|
```
|
74
85
|
|
75
86
|
It prints some output like this:
|
data/docs/threadpool.md
CHANGED
@@ -1,9 +1,12 @@
|
|
1
|
+
## A thread-pool implementation making use of Eventbox
|
2
|
+
|
1
3
|
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.
|
4
|
+
It shows how the action method `start_pool_thread` makes use of the private {Eventbox.yield_call yield_call} `next_job` to query, wait for and retrieve an object from the event scope.
|
3
5
|
|
4
6
|
This kind of object is the block that is given to `pool`.
|
5
7
|
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
8
|
When this happens, the wrapping is automatically removed, so that the pure block given to `pool` is called in `start_pool_thread`.
|
9
|
+
That way each action thread runs one block at the same time, but all started action threads process the blocks concurrently.
|
7
10
|
|
8
11
|
```ruby
|
9
12
|
class ThreadPool < Eventbox
|
data/eventbox.gemspec
CHANGED
@@ -21,11 +21,12 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.bindir = "exe"
|
22
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
23
|
spec.require_paths = ["lib"]
|
24
|
-
spec.required_ruby_version = "
|
24
|
+
spec.required_ruby_version = ">= 3.0", "< 4.0"
|
25
25
|
spec.metadata["yard.run"] = "yri" # use "yard" to build full HTML docs.
|
26
26
|
|
27
27
|
spec.add_development_dependency "bundler", ">= 1.16", "< 3"
|
28
|
-
spec.add_development_dependency "rake", "~>
|
28
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
29
29
|
spec.add_development_dependency "minitest", "~> 5.0"
|
30
|
+
spec.add_development_dependency "minitest-hooks"
|
30
31
|
spec.add_development_dependency "yard", "~> 0.9"
|
31
32
|
end
|
data/lib/eventbox.rb
CHANGED
@@ -2,10 +2,11 @@
|
|
2
2
|
|
3
3
|
require "weakref"
|
4
4
|
require "eventbox/argument_wrapper"
|
5
|
-
require "eventbox/
|
5
|
+
require "eventbox/call_context"
|
6
6
|
require "eventbox/boxable"
|
7
7
|
require "eventbox/event_loop"
|
8
8
|
require "eventbox/object_registry"
|
9
|
+
require "eventbox/sanitizer"
|
9
10
|
|
10
11
|
class Eventbox
|
11
12
|
autoload :VERSION, "eventbox/version"
|
@@ -18,7 +19,7 @@ class Eventbox
|
|
18
19
|
class MultipleResults < RuntimeError; end
|
19
20
|
class AbortAction < RuntimeError; end
|
20
21
|
|
21
|
-
if RUBY_ENGINE=='jruby' &&
|
22
|
+
if RUBY_ENGINE=='jruby' && JRUBY_VERSION.split(".").map(&:to_i).pack("C*") < [9,2,1,0].pack("C*") ||
|
22
23
|
RUBY_ENGINE=='truffleruby'
|
23
24
|
# This is a workaround for bug https://github.com/jruby/jruby/issues/5314
|
24
25
|
# which was fixed in JRuby-9.2.1.0.
|
@@ -90,7 +91,7 @@ class Eventbox
|
|
90
91
|
# Create a new {Eventbox} instance.
|
91
92
|
#
|
92
93
|
# All arguments are passed to the init() method when defined.
|
93
|
-
def initialize(*args, &block)
|
94
|
+
def initialize(*args, **kwargs, &block)
|
94
95
|
options = self.class.eventbox_options
|
95
96
|
|
96
97
|
# This instance variable is set to self here, but replaced by Boxable#action to a WeakRef
|
@@ -104,7 +105,7 @@ class Eventbox
|
|
104
105
|
self.class.instance_variable_set(:@eventbox_methods_checked, true)
|
105
106
|
|
106
107
|
obj = Object.new
|
107
|
-
meths = methods - obj.methods - [:__getobj__, :shutdown!, :shared_object]
|
108
|
+
meths = methods - obj.methods - [:__getobj__, :shutdown!, :shared_object, :€]
|
108
109
|
prmeths = private_methods - obj.private_methods
|
109
110
|
prohib = meths.find do |name|
|
110
111
|
!prmeths.include?(:"__#{name}__")
|
@@ -120,7 +121,7 @@ class Eventbox
|
|
120
121
|
@__event_loop__ = EventLoop.new(options[:threadpool], options[:guard_time])
|
121
122
|
ObjectSpace.define_finalizer(self, @__event_loop__.method(:send_shutdown))
|
122
123
|
|
123
|
-
init(*args, &block)
|
124
|
+
init(*args, **kwargs, &block)
|
124
125
|
end
|
125
126
|
|
126
127
|
def self.method_added(name)
|
@@ -240,19 +241,70 @@ class Eventbox
|
|
240
241
|
# Mark an object as to be shared instead of copied.
|
241
242
|
#
|
242
243
|
# 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
|
-
#
|
245
|
-
#
|
246
|
-
#
|
244
|
+
# The object is therefore wrapped as {WrappedObject} or {ExternalObject} when used in an unsafe scope.
|
245
|
+
# Objects marked within the event scope are wrapped as {WrappedObject}, which denies access from external/action scope or the event scope of a different Eventbox instance.
|
246
|
+
# Objects marked in external/action scope are wrapped as {ExternalObject} which allows {External.send asynchronous calls} from event scope.
|
247
|
+
# In all cases the object can be passed as reference and is automatically unwrapped when passed back to the original scope.
|
247
248
|
# It can therefore be used to modify the original object even after traversing the boundaries.
|
248
249
|
#
|
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
250
|
# The mark is stored for the lifetime of the object, so that it's enough to mark only once at object creation.
|
251
|
+
#
|
252
|
+
# Due to {Eventbox::Sanitizer Sanitizer dissection} of non-marshalable objects, wrapping and unwrapping works even if the shared object is stored within another object as instance variable or within a collection class.
|
253
|
+
# This is in contrast to €-variables which can only wrap the argument object as a whole when entering the event scope.
|
254
|
+
# See the difference here:
|
255
|
+
#
|
256
|
+
# A = Struct.new(:a)
|
257
|
+
# class Bo < Eventbox
|
258
|
+
# sync_call def go(struct, €struct)
|
259
|
+
# p struct # prints #<struct A a=#<Eventbox::ExternalObject @object="abc" @name=:a>>
|
260
|
+
# p €struct # prints #<Eventbox::ExternalObject @object=#<struct A a="abc"> @name=:€struct>
|
261
|
+
# [struct, €struct]
|
262
|
+
# end
|
263
|
+
# end
|
264
|
+
# e = Bo.new
|
265
|
+
# o = A.new(e.shared_object("abc"))
|
266
|
+
# e.go(o, o) # => [#<struct A a="abc">, #<struct A a="abc">]
|
252
267
|
public def shared_object(object)
|
253
268
|
@__event_loop__.shared_object(object)
|
254
269
|
end
|
255
270
|
|
271
|
+
public def €(object)
|
272
|
+
@__event_loop__.€(object)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Starts a new action dedicated to call external objects.
|
276
|
+
#
|
277
|
+
# It returns a {CallContext} which can be used with {Eventbox::ExternalObject#send} and {Eventbox::ExternalProc#call}.
|
278
|
+
#
|
279
|
+
# @returns [ActionCallContext] A new call context provided by a newly started action.
|
280
|
+
private def new_action_call_context
|
281
|
+
ActionCallContext.new(@__event_loop__)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Get the context of the waiting external call within a yield or sync method or closure.
|
285
|
+
#
|
286
|
+
# Callable within the event scope only.
|
287
|
+
#
|
288
|
+
# @returns [BlockingExternalCallContext] The current call context.
|
289
|
+
# Returns +nil+ in async_call or async_proc context.
|
290
|
+
#
|
291
|
+
# Usable as first parameter to {ExternalProc.call} and {ExternalObject.send}.
|
292
|
+
private def call_context
|
293
|
+
if @__event_loop__.event_scope?
|
294
|
+
@__event_loop__._latest_call_context
|
295
|
+
else
|
296
|
+
raise InvalidAccess, "not in event scope"
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
private def with_call_context(ctx, &block)
|
301
|
+
if @__event_loop__.event_scope?
|
302
|
+
@__event_loop__.with_call_context(ctx, &block)
|
303
|
+
else
|
304
|
+
raise InvalidAccess, "not in event scope"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
256
308
|
# Force stop of all action threads spawned by this {Eventbox} instance.
|
257
309
|
#
|
258
310
|
# It is possible to enable automatic cleanup of action threads by the garbage collector through {Eventbox.with_options}.
|
@@ -21,54 +21,55 @@ class Eventbox
|
|
21
21
|
decls = []
|
22
22
|
convs = []
|
23
23
|
rets = []
|
24
|
+
kwrets = []
|
24
25
|
parameters.each_with_index do |(t, n), i|
|
25
26
|
€var = n.to_s.start_with?("€")
|
26
27
|
case t
|
27
28
|
when :req
|
28
29
|
decls << n
|
29
30
|
if €var
|
30
|
-
convs << "#{n} =
|
31
|
+
convs << "#{n} = Sanitizer.wrap_object(#{n}, source_event_loop, target_event_loop, :#{n})"
|
31
32
|
end
|
32
33
|
rets << n
|
33
34
|
when :opt
|
34
35
|
decls << "#{n}=nil"
|
35
36
|
if €var
|
36
|
-
convs << "#{n} = #{n} ?
|
37
|
+
convs << "#{n} = #{n} ? Sanitizer.wrap_object(#{n}, source_event_loop, target_event_loop, :#{n}) : []"
|
37
38
|
end
|
38
39
|
rets << "*#{n}"
|
39
40
|
when :rest
|
40
41
|
decls << "*#{n}"
|
41
42
|
if €var
|
42
|
-
convs << "#{n}.map!{|v|
|
43
|
+
convs << "#{n}.map!{|v| Sanitizer.wrap_object(v, source_event_loop, target_event_loop, :#{n}) }"
|
43
44
|
end
|
44
45
|
rets << "*#{n}"
|
45
46
|
when :keyreq
|
46
47
|
decls << "#{n}:"
|
47
48
|
if €var
|
48
|
-
convs << "#{n} =
|
49
|
+
convs << "#{n} = Sanitizer.wrap_object(#{n}, source_event_loop, target_event_loop, :#{n})"
|
49
50
|
end
|
50
|
-
|
51
|
+
kwrets << "#{n}: #{n}"
|
51
52
|
when :key
|
52
53
|
decls << "#{n}:nil"
|
53
54
|
if €var
|
54
|
-
convs << "#{n} = #{n} ? {#{n}:
|
55
|
+
convs << "#{n} = #{n} ? {#{n}: Sanitizer.wrap_object(#{n}, source_event_loop, target_event_loop, :#{n})} : {}"
|
55
56
|
else
|
56
57
|
convs << "#{n} = #{n} ? {#{n}: #{n}} : {}"
|
57
58
|
end
|
58
|
-
|
59
|
+
kwrets << "**#{n}"
|
59
60
|
when :keyrest
|
60
61
|
decls << "**#{n}"
|
61
62
|
if €var
|
62
|
-
convs << "#{n}.
|
63
|
+
convs << "#{n}.transform_values!{|v| Sanitizer.wrap_object(v, source_event_loop, target_event_loop, :#{n}) }"
|
63
64
|
end
|
64
|
-
|
65
|
+
kwrets << "**#{n}"
|
65
66
|
when :block
|
66
67
|
if €var
|
67
68
|
raise "block to `#{name}' can't be wrapped"
|
68
69
|
end
|
69
70
|
end
|
70
71
|
end
|
71
|
-
code = "#{is_proc ? :proc : :lambda} do |source_event_loop#{decls.map{|s| ",#{s}"}.join }| # #{name}\n #{convs.join("\n")}\n [#{rets.join(",")}]\nend"
|
72
|
+
code = "#{is_proc ? :proc : :lambda} do |source_event_loop, target_event_loop#{decls.map{|s| ",#{s}"}.join }| # #{name}\n #{convs.join("\n")}\n [[#{rets.join(",")}],{#{kwrets.join(",")}}]\nend"
|
72
73
|
instance_eval(code, "wrapper code defined in #{__FILE__}:#{__LINE__} for #{name}")
|
73
74
|
end
|
74
75
|
end
|
data/lib/eventbox/boxable.rb
CHANGED
@@ -36,7 +36,7 @@ class Eventbox
|
|
36
36
|
#
|
37
37
|
# The created method can be safely called from any thread.
|
38
38
|
# All method arguments are passed through the {Sanitizer}.
|
39
|
-
# Arguments prefixed by a
|
39
|
+
# Arguments prefixed by a +€+ sign are automatically passed as {Eventbox::ExternalObject}.
|
40
40
|
#
|
41
41
|
# The method itself might not do any blocking calls or expensive computations - this would impair responsiveness of the {Eventbox} instance.
|
42
42
|
# Instead use {action} in these cases.
|
@@ -47,13 +47,13 @@ class Eventbox
|
|
47
47
|
def async_call(name, &block)
|
48
48
|
unbound_method = self.instance_method(name)
|
49
49
|
wrapper = ArgumentWrapper.build(unbound_method, name)
|
50
|
-
with_block_or_def(name, block) do |*args, &cb|
|
50
|
+
with_block_or_def(name, block) do |*args, **kwargs, &cb|
|
51
51
|
if @__event_loop__.event_scope?
|
52
52
|
# Use the correct method within the class hierarchy, instead of just self.send(*args).
|
53
53
|
# Otherwise super() would start an infinite recursion.
|
54
|
-
unbound_method.bind(eventbox).call(*args, &cb)
|
54
|
+
unbound_method.bind(eventbox).call(*args, **kwargs, &cb)
|
55
55
|
else
|
56
|
-
@__event_loop__.async_call(eventbox, name, args, cb, wrapper)
|
56
|
+
@__event_loop__.async_call(eventbox, name, args, kwargs, cb, wrapper)
|
57
57
|
end
|
58
58
|
self
|
59
59
|
end
|
@@ -70,20 +70,20 @@ class Eventbox
|
|
70
70
|
# Blocks are executed by the same thread that calls the {sync_call} method to that time.
|
71
71
|
#
|
72
72
|
# All method arguments as well as the result value are passed through the {Sanitizer}.
|
73
|
-
# Arguments prefixed by a
|
73
|
+
# Arguments prefixed by a +€+ sign are automatically passed as {Eventbox::ExternalObject}.
|
74
74
|
#
|
75
75
|
# The method itself might not do any blocking calls or expensive computations - this would impair responsiveness of the {Eventbox} instance.
|
76
76
|
# Instead use {action} in these cases.
|
77
77
|
def sync_call(name, &block)
|
78
78
|
unbound_method = self.instance_method(name)
|
79
79
|
wrapper = ArgumentWrapper.build(unbound_method, name)
|
80
|
-
with_block_or_def(name, block) do |*args, &cb|
|
80
|
+
with_block_or_def(name, block) do |*args, **kwargs, &cb|
|
81
81
|
if @__event_loop__.event_scope?
|
82
|
-
unbound_method.bind(eventbox).call(*args, &cb)
|
82
|
+
unbound_method.bind(eventbox).call(*args, **kwargs, &cb)
|
83
83
|
else
|
84
84
|
answer_queue = Queue.new
|
85
|
-
sel = @__event_loop__.sync_call(eventbox, name, args, cb, answer_queue, wrapper)
|
86
|
-
@__event_loop__.callback_loop(answer_queue, sel)
|
85
|
+
sel = @__event_loop__.sync_call(eventbox, name, args, kwargs, cb, answer_queue, wrapper)
|
86
|
+
@__event_loop__.callback_loop(answer_queue, sel, name)
|
87
87
|
end
|
88
88
|
end
|
89
89
|
end
|
@@ -106,7 +106,7 @@ class Eventbox
|
|
106
106
|
# Blocks are executed by the same thread that calls the {yield_call} method to that time.
|
107
107
|
#
|
108
108
|
# All method arguments as well as the result value are passed through the {Sanitizer}.
|
109
|
-
# Arguments prefixed by a
|
109
|
+
# Arguments prefixed by a +€+ sign are automatically passed as {Eventbox::ExternalObject}.
|
110
110
|
#
|
111
111
|
# The method itself as well as the Proc object might not do any blocking calls or expensive computations - this would impair responsiveness of the {Eventbox} instance.
|
112
112
|
# Instead use {action} in these cases.
|
@@ -115,37 +115,38 @@ class Eventbox
|
|
115
115
|
wrapper = ArgumentWrapper.build(unbound_method, name)
|
116
116
|
with_block_or_def(name, block) do |*args, **kwargs, &cb|
|
117
117
|
if @__event_loop__.event_scope?
|
118
|
-
@__event_loop__.
|
119
|
-
args
|
120
|
-
unbound_method.bind(eventbox).call(*args, &cb)
|
118
|
+
@__event_loop__.internal_yield_result(args, name)
|
119
|
+
unbound_method.bind(eventbox).call(*args, **kwargs, &cb)
|
121
120
|
self
|
122
121
|
else
|
123
122
|
answer_queue = Queue.new
|
124
123
|
sel = @__event_loop__.yield_call(eventbox, name, args, kwargs, cb, answer_queue, wrapper)
|
125
|
-
@__event_loop__.callback_loop(answer_queue, sel)
|
124
|
+
@__event_loop__.callback_loop(answer_queue, sel, name)
|
126
125
|
end
|
127
126
|
end
|
128
127
|
end
|
129
128
|
|
130
129
|
# Threadsafe write access to instance variables.
|
131
|
-
def attr_writer(
|
132
|
-
|
133
|
-
|
134
|
-
|
130
|
+
def attr_writer(*names)
|
131
|
+
super
|
132
|
+
names.each do |name|
|
133
|
+
async_call(:"#{name}=")
|
134
|
+
end
|
135
135
|
end
|
136
136
|
|
137
137
|
# Threadsafe read access to instance variables.
|
138
|
-
def attr_reader(
|
139
|
-
|
140
|
-
|
141
|
-
|
138
|
+
def attr_reader(*names)
|
139
|
+
super
|
140
|
+
names.each do |name|
|
141
|
+
sync_call(:"#{name}")
|
142
|
+
end
|
142
143
|
end
|
143
144
|
|
144
145
|
# Threadsafe read and write access to instance variables.
|
145
146
|
#
|
146
|
-
# Attention: Be careful with read-modify-write operations - they are *not* atomic but are executed as two independent operations.
|
147
|
+
# Attention: Be careful with read-modify-write operations like "+=" - they are *not* atomic but are executed as two independent operations.
|
147
148
|
#
|
148
|
-
# This will lose counter increments, since
|
149
|
+
# This will lose counter increments, since +counter+ is incremented in a non-atomic manner:
|
149
150
|
# attr_accessor :counter
|
150
151
|
# async_call def start
|
151
152
|
# 10.times { do_something }
|
@@ -164,9 +165,12 @@ class Eventbox
|
|
164
165
|
# async_call def increment(by)
|
165
166
|
# @counter += by
|
166
167
|
# end
|
167
|
-
def attr_accessor(
|
168
|
-
|
169
|
-
|
168
|
+
def attr_accessor(*names)
|
169
|
+
super
|
170
|
+
names.each do |name|
|
171
|
+
async_call(:"#{name}=")
|
172
|
+
sync_call(:"#{name}")
|
173
|
+
end
|
170
174
|
end
|
171
175
|
|
172
176
|
# Define a private method for asynchronous execution.
|
@@ -192,7 +196,7 @@ class Eventbox
|
|
192
196
|
# str # => "value1"
|
193
197
|
# action.current? # => true
|
194
198
|
# # `action' can be passed to event scope or external scope,
|
195
|
-
# # in order to send a
|
199
|
+
# # in order to send a signal per Action#raise
|
196
200
|
# end
|
197
201
|
#
|
198
202
|
def action(name, &block)
|
@@ -219,7 +223,7 @@ class Eventbox
|
|
219
223
|
|
220
224
|
# An Action object is thin wrapper for a Ruby thread.
|
221
225
|
#
|
222
|
-
# It is returned by {Eventbox#action} and optionally passed as last argument to action methods.
|
226
|
+
# It is returned by {Eventbox::Boxable#action action methods} and optionally passed as last argument to action methods.
|
223
227
|
# It can be used to interrupt the program execution by an exception.
|
224
228
|
#
|
225
229
|
# However in contrast to ruby's builtin threads, any interruption must be explicit allowed.
|
@@ -228,7 +232,7 @@ class Eventbox
|
|
228
232
|
# It is raised by {Eventbox#shutdown!} and is delivered as soon as a blocking operation is executed.
|
229
233
|
#
|
230
234
|
# An Action object can be used to stop the action while blocking operations.
|
231
|
-
# It should be made sure, that the
|
235
|
+
# It should be made sure, that the +rescue+ statement is outside of the block to +handle_interrupt+.
|
232
236
|
# Otherwise it could happen, that the rescuing code is interrupted by the signal.
|
233
237
|
# Sending custom signals to an action works like:
|
234
238
|
#
|
@@ -266,10 +270,9 @@ class Eventbox
|
|
266
270
|
#
|
267
271
|
# This method does nothing if the action is already finished.
|
268
272
|
#
|
269
|
-
# If {raise} is called within the action (#current? returns
|
270
|
-
# This happens regardless of the current interrupt mask set by
|
273
|
+
# If {raise} is called within the action ({#current?} returns +true+), all exceptions are delivered immediately.
|
274
|
+
# This happens regardless of the current interrupt mask set by +Thread.handle_interrupt+.
|
271
275
|
def raise(*args)
|
272
|
-
# ignore raise, if sent from the action thread
|
273
276
|
if AbortAction === args[0] || (Module === args[0] && args[0].ancestors.include?(AbortAction))
|
274
277
|
::Kernel.raise InvalidAccess, "Use of Eventbox::AbortAction is not allowed - use Action#abort or a custom exception subclass"
|
275
278
|
end
|
@@ -294,5 +297,10 @@ class Eventbox
|
|
294
297
|
def join
|
295
298
|
@thread.join
|
296
299
|
end
|
300
|
+
|
301
|
+
# @private
|
302
|
+
def terminate
|
303
|
+
@thread.terminate
|
304
|
+
end
|
297
305
|
end
|
298
306
|
end
|