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.
@@ -0,0 +1 @@
1
+ [![MyQueue calls](https://raw.github.com/larskanis/eventbox/master/docs/images/my_queue_calls.svg?sanitize=true)](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 = "~> 2.3"
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", "~> 10.0"
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/sanitizer"
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' && RUBY_VERSION.split(".").map(&:to_i).pack("C*") < [9,2,1,0].pack("C*") ||
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
- # 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.
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} = WrappedObject.new(#{n}, source_event_loop, :#{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} ? WrappedObject.new(#{n}, source_event_loop, :#{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| WrappedObject.new(v, source_event_loop, :#{n}) }"
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} = WrappedObject.new(#{n}, source_event_loop, :#{n})"
49
+ convs << "#{n} = Sanitizer.wrap_object(#{n}, source_event_loop, target_event_loop, :#{n})"
49
50
  end
50
- rets << "#{n}: #{n}"
51
+ kwrets << "#{n}: #{n}"
51
52
  when :key
52
53
  decls << "#{n}:nil"
53
54
  if €var
54
- convs << "#{n} = #{n} ? {#{n}: WrappedObject.new(#{n}, source_event_loop, :#{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
- rets << "**#{n}"
59
+ kwrets << "**#{n}"
59
60
  when :keyrest
60
61
  decls << "**#{n}"
61
62
  if €var
62
- convs << "#{n}.each{|k, v| #{n}[k] = WrappedObject.new(v, source_event_loop, :#{n}) }"
63
+ convs << "#{n}.transform_values!{|v| Sanitizer.wrap_object(v, source_event_loop, target_event_loop, :#{n}) }"
63
64
  end
64
- rets << "**#{n}"
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
@@ -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 sign are automatically passed as {Eventbox::WrappedObject}.
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 sign are automatically passed as {Eventbox::WrappedObject}.
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 sign are automatically passed as {Eventbox::WrappedObject}.
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__.safe_yield_result(args, name)
119
- args << kwargs unless kwargs.empty?
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(name)
132
- async_call(define_method("#{name}=") do |value|
133
- instance_variable_set("@#{name}", value)
134
- end)
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(name)
139
- sync_call(define_method("#{name}") do
140
- instance_variable_get("@#{name}")
141
- end)
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 `counter` is incremented in a non-atomic manner:
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(name)
168
- attr_reader name
169
- attr_writer name
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 signals per Action#raise
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 `rescue` statement is outside of the block to `handle_interrupt`.
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 `true`), all exceptions are delivered immediately.
270
- # This happens regardless of the current interrupt mask set by `Thread.handle_interrupt`.
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