eventbox 0.1.0 → 1.0.0

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