eventbox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ # frozen-string-literal: true
2
+
3
+ class Eventbox
4
+ module ArgumentWrapper
5
+ def self.build(method, name)
6
+ parameters = method.parameters
7
+ if parameters.find { |t, n| n.to_s.start_with?("€") }
8
+
9
+ # Change a Proc object to a Method, so that we are able to differ between :opt and :req parameters.
10
+ # This is because Ruby (wrongly IMHO) reports required parameters as optional.
11
+ # The only way to get the true parameter types is through define_method.
12
+ is_proc = Proc === method
13
+ if is_proc
14
+ cl = Class.new do
15
+ define_method(:to_method, &method)
16
+ end
17
+ method = cl.instance_method(:to_method)
18
+ parameters = method.parameters
19
+ end
20
+
21
+ decls = []
22
+ convs = []
23
+ rets = []
24
+ parameters.each_with_index do |(t, n), i|
25
+ €var = n.to_s.start_with?("€")
26
+ case t
27
+ when :req
28
+ decls << n
29
+ if €var
30
+ convs << "#{n} = WrappedObject.new(#{n}, source_event_loop, :#{n})"
31
+ end
32
+ rets << n
33
+ when :opt
34
+ decls << "#{n}=nil"
35
+ if €var
36
+ convs << "#{n} = #{n} ? WrappedObject.new(#{n}, source_event_loop, :#{n}) : []"
37
+ end
38
+ rets << "*#{n}"
39
+ when :rest
40
+ decls << "*#{n}"
41
+ if €var
42
+ convs << "#{n}.map!{|v| WrappedObject.new(v, source_event_loop, :#{n}) }"
43
+ end
44
+ rets << "*#{n}"
45
+ when :keyreq
46
+ decls << "#{n}:"
47
+ if €var
48
+ convs << "#{n} = WrappedObject.new(#{n}, source_event_loop, :#{n})"
49
+ end
50
+ rets << "#{n}: #{n}"
51
+ when :key
52
+ decls << "#{n}:nil"
53
+ if €var
54
+ convs << "#{n} = #{n} ? {#{n}: WrappedObject.new(#{n}, source_event_loop, :#{n})} : {}"
55
+ else
56
+ convs << "#{n} = #{n} ? {#{n}: #{n}} : {}"
57
+ end
58
+ rets << "**#{n}"
59
+ when :keyrest
60
+ decls << "**#{n}"
61
+ if €var
62
+ convs << "#{n}.each{|k, v| #{n}[k] = WrappedObject.new(v, source_event_loop, :#{n}) }"
63
+ end
64
+ rets << "**#{n}"
65
+ when :block
66
+ if €var
67
+ raise "block to `#{name}' can't be wrapped"
68
+ end
69
+ end
70
+ 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
+ instance_eval(code, "wrapper code defined in #{__FILE__}:#{__LINE__} for #{name}")
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,298 @@
1
+ # frozen-string-literal: true
2
+
3
+ class Eventbox
4
+
5
+ # Extend modules with Eventbox method creation functions
6
+ #
7
+ # This works like so:
8
+ #
9
+ # module MyHelpers
10
+ # extend Eventbox::Boxable
11
+ # sync_call def hello
12
+ # puts "hello!"
13
+ # end
14
+ # end
15
+ #
16
+ # class MyBox < Eventbox
17
+ # include MyHelpers
18
+ # end
19
+ #
20
+ # MyBox.new.hello # prints "hello!"
21
+ #
22
+ module Boxable
23
+ private
24
+
25
+ # @private
26
+ def with_block_or_def(name, block, &cexec)
27
+ alias_method("__#{name}__", name)
28
+ private("__#{name}__")
29
+ remove_method(name)
30
+ define_method(name, &cexec)
31
+ private name if name == :init
32
+ name
33
+ end
34
+
35
+ # Define a threadsafe method for asynchronous (fire-and-forget) calls.
36
+ #
37
+ # The created method can be safely called from any thread.
38
+ # All method arguments are passed through the {Sanitizer}.
39
+ # Arguments prefixed by a € sign are automatically passed as {Eventbox::WrappedObject}.
40
+ #
41
+ # The method itself might not do any blocking calls or expensive computations - this would impair responsiveness of the {Eventbox} instance.
42
+ # Instead use {action} in these cases.
43
+ #
44
+ # In contrast to {sync_call} it's not possible to call external blocks or proc objects from {async_call} methods.
45
+ #
46
+ # The method always returns +self+ to the caller.
47
+ def async_call(name, &block)
48
+ unbound_method = self.instance_method(name)
49
+ wrapper = ArgumentWrapper.build(unbound_method, name)
50
+ with_block_or_def(name, block) do |*args, &cb|
51
+ if @__event_loop__.event_scope?
52
+ # Use the correct method within the class hierarchy, instead of just self.send(*args).
53
+ # Otherwise super() would start an infinite recursion.
54
+ unbound_method.bind(eventbox).call(*args, &cb)
55
+ else
56
+ @__event_loop__.async_call(eventbox, name, args, cb, wrapper)
57
+ end
58
+ self
59
+ end
60
+ end
61
+
62
+ # Define a method for synchronous calls.
63
+ #
64
+ # The created method can be safely called from any thread.
65
+ # It is simular to {async_call}, but the method waits until the method body is executed and returns its return value.
66
+ # Since all processing within the event scope of an {Eventbox} instance must not involve blocking operations, sync calls can only return immediate values.
67
+ # For deferred results use {yield_call} instead.
68
+ #
69
+ # It's possible to call external blocks or proc objects from {sync_call} methods.
70
+ # Blocks are executed by the same thread that calls the {sync_call} method to that time.
71
+ #
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}.
74
+ #
75
+ # The method itself might not do any blocking calls or expensive computations - this would impair responsiveness of the {Eventbox} instance.
76
+ # Instead use {action} in these cases.
77
+ def sync_call(name, &block)
78
+ unbound_method = self.instance_method(name)
79
+ wrapper = ArgumentWrapper.build(unbound_method, name)
80
+ with_block_or_def(name, block) do |*args, &cb|
81
+ if @__event_loop__.event_scope?
82
+ unbound_method.bind(eventbox).call(*args, &cb)
83
+ else
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)
87
+ end
88
+ end
89
+ end
90
+
91
+ # Define a method for calls with deferred result.
92
+ #
93
+ # This call type is simular to {sync_call}, however it's not the result of the method that is returned.
94
+ # Instead the method is called with one {CompletionProc additional argument} in the event scope, which is used to yield a result value or raise an exception.
95
+ # In contrast to a +return+ statement, the execution of the method continues after yielding a result.
96
+ #
97
+ # The result value can be yielded within the called method, but it can also be stored and called by any other event scope or external method, leading to a deferred method return.
98
+ # The external thread calling this method is suspended until a result is yielded.
99
+ # However the Eventbox object keeps responsive to calls from other threads.
100
+ #
101
+ # The created method can be safely called from any thread.
102
+ # If yield methods are called in the event scope, they must get a Proc object as the last argument.
103
+ # It is called when a result was yielded.
104
+ #
105
+ # It's possible to call external blocks or proc objects from {yield_call} methods up to the point when the result was yielded.
106
+ # Blocks are executed by the same thread that calls the {yield_call} method to that time.
107
+ #
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}.
110
+ #
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
+ # Instead use {action} in these cases.
113
+ def yield_call(name, &block)
114
+ unbound_method = self.instance_method(name)
115
+ wrapper = ArgumentWrapper.build(unbound_method, name)
116
+ with_block_or_def(name, block) do |*args, **kwargs, &cb|
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)
121
+ self
122
+ else
123
+ answer_queue = Queue.new
124
+ sel = @__event_loop__.yield_call(eventbox, name, args, kwargs, cb, answer_queue, wrapper)
125
+ @__event_loop__.callback_loop(answer_queue, sel)
126
+ end
127
+ end
128
+ end
129
+
130
+ # 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)
135
+ end
136
+
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)
142
+ end
143
+
144
+ # Threadsafe read and write access to instance variables.
145
+ #
146
+ # Attention: Be careful with read-modify-write operations - they are *not* atomic but are executed as two independent operations.
147
+ #
148
+ # This will lose counter increments, since `counter` is incremented in a non-atomic manner:
149
+ # attr_accessor :counter
150
+ # async_call def start
151
+ # 10.times { do_something }
152
+ # end
153
+ # action def do_something
154
+ # self.counter += 1
155
+ # end
156
+ #
157
+ # Instead don't use accessors but do increments within one method call like so:
158
+ # async_call def start
159
+ # 10.times { do_something }
160
+ # end
161
+ # action def do_something
162
+ # increment 1
163
+ # end
164
+ # async_call def increment(by)
165
+ # @counter += by
166
+ # end
167
+ def attr_accessor(name)
168
+ attr_reader name
169
+ attr_writer name
170
+ end
171
+
172
+ # Define a private method for asynchronous execution.
173
+ #
174
+ # The call to the action method returns immediately after starting a new action.
175
+ # It returns an {Action} object.
176
+ # By default each call to an action method spawns a new thread which executes the code of the action definition.
177
+ # Alternatively a threadpool can be assigned by {with_options}.
178
+ #
179
+ # All method arguments are passed through the {Sanitizer}.
180
+ #
181
+ # Actions can return state changes or objects to the event loop by calls to methods created by {async_call}, {sync_call} or {yield_call} or through calling {async_proc}, {sync_proc} or {yield_proc} objects.
182
+ # To avoid unsafe shared objects, an action has it's own set of local variables or instance variables.
183
+ # It doesn't have access to variables defined by other methods.
184
+ #
185
+ # The {Action} object can be used to interrupt the program execution by an exception.
186
+ # See {Eventbox::Action} for further information.
187
+ # If the action method accepts one more argument than given to the action call, it is set to corresponding {Action} instance:
188
+ # async_call def init
189
+ # do_something("value1")
190
+ # end
191
+ # action def do_something(str, action)
192
+ # str # => "value1"
193
+ # action.current? # => true
194
+ # # `action' can be passed to event scope or external scope,
195
+ # # in order to send a signals per Action#raise
196
+ # end
197
+ #
198
+ def action(name, &block)
199
+ unbound_method = self.instance_method(name)
200
+ with_block_or_def(name, block) do |*args, &cb|
201
+ raise InvalidAccess, "action must not be called with a block" if cb
202
+
203
+ gc_actions = self.class.eventbox_options[:gc_actions]
204
+ sandbox = self.class.allocate
205
+ sandbox.instance_variable_set(:@__event_loop__, @__event_loop__)
206
+ sandbox.instance_variable_set(:@__eventbox__, gc_actions ? WeakRef.new(self) : self)
207
+ meth = unbound_method.bind(sandbox)
208
+
209
+ if @__event_loop__.event_scope?
210
+ args = Sanitizer.sanitize_values(args, @__event_loop__, nil)
211
+ end
212
+ # Start a new action thread and return an Action instance
213
+ @__event_loop__.start_action(meth, name, args)
214
+ end
215
+ private name
216
+ name
217
+ end
218
+ end
219
+
220
+ # An Action object is thin wrapper for a Ruby thread.
221
+ #
222
+ # It is returned by {Eventbox#action} and optionally passed as last argument to action methods.
223
+ # It can be used to interrupt the program execution by an exception.
224
+ #
225
+ # However in contrast to ruby's builtin threads, any interruption must be explicit allowed.
226
+ # Exceptions raised to an action thread are delayed until a code block is reached which explicit allows interruption.
227
+ # The only exception which is delivered to the action thread by default is {Eventbox::AbortAction}.
228
+ # It is raised by {Eventbox#shutdown!} and is delivered as soon as a blocking operation is executed.
229
+ #
230
+ # 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`.
232
+ # Otherwise it could happen, that the rescuing code is interrupted by the signal.
233
+ # Sending custom signals to an action works like:
234
+ #
235
+ # class MySignal < Interrupt
236
+ # end
237
+ #
238
+ # async_call def init
239
+ # a = start_sleep
240
+ # a.raise(MySignal)
241
+ # end
242
+ #
243
+ # action def start_sleep
244
+ # Thread.handle_interrupt(MySignal => :on_blocking) do
245
+ # sleep
246
+ # end
247
+ # rescue MySignal
248
+ # puts "well-rested"
249
+ # end
250
+ class Action
251
+ attr_reader :name
252
+
253
+ def initialize(name, thread, event_loop)
254
+ @name = name
255
+ @thread = thread
256
+ @event_loop = event_loop
257
+ end
258
+
259
+ attr_reader :event_loop
260
+ private :event_loop
261
+
262
+ # Send a signal to the running action.
263
+ #
264
+ # The signal must be kind of Exception.
265
+ # See {Action} about asynchronous delivery of signals.
266
+ #
267
+ # This method does nothing if the action is already finished.
268
+ #
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`.
271
+ def raise(*args)
272
+ # ignore raise, if sent from the action thread
273
+ if AbortAction === args[0] || (Module === args[0] && args[0].ancestors.include?(AbortAction))
274
+ ::Kernel.raise InvalidAccess, "Use of Eventbox::AbortAction is not allowed - use Action#abort or a custom exception subclass"
275
+ end
276
+
277
+ if @event_loop.event_scope?
278
+ args = Sanitizer.sanitize_values(args, @event_loop, nil)
279
+ end
280
+ @thread.raise(*args)
281
+ end
282
+
283
+ # Send a AbortAction to the running thread.
284
+ def abort
285
+ @thread.raise AbortAction
286
+ end
287
+
288
+ # Belongs the current thread to this action.
289
+ def current?
290
+ @thread.respond_to?(:current?) ? @thread.current? : (@thread == Thread.current)
291
+ end
292
+
293
+ # @private
294
+ def join
295
+ @thread.join
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,385 @@
1
+ # frozen-string-literal: true
2
+
3
+ class Eventbox
4
+ # @private
5
+ #
6
+ # This class manages the calls to event scope methods and procs comparable to an event loop.
7
+ # It doesn't use an explicit event loop, but uses the calling thread to process the event.
8
+ #
9
+ # All methods prefixed with "_" requires @mutex acquired to be called.
10
+ class EventLoop
11
+ def initialize(threadpool, guard_time)
12
+ @threadpool = threadpool
13
+ @running_actions = []
14
+ @running_actions_for_gc = []
15
+ @mutex = Mutex.new
16
+ @shutdown = false
17
+ @guard_time_proc = case guard_time
18
+ when NilClass
19
+ nil
20
+ when Numeric
21
+ guard_time and proc do |dt, name|
22
+ if dt > guard_time
23
+ ecaller = caller.find{|t| !(t=~/lib\/eventbox(\/|\.rb:)/) }
24
+ warn "guard time exceeded: #{"%2.3f" % dt} sec (limit is #{guard_time}) in `#{name}' called from `#{ecaller}' - please move blocking tasks to actions"
25
+ end
26
+ end
27
+ when Proc
28
+ guard_time
29
+ else
30
+ raise ArgumentError, "guard_time should be Numeric, Proc or nil"
31
+ end
32
+ end
33
+
34
+ # Abort all running action threads.
35
+ def send_shutdown(object_id=nil)
36
+ # warn "shutdown called for object #{object_id} with #{@running_actions.size} threads #{@running_actions.map(&:object_id).join(",")}"
37
+
38
+ # The finalizer doesn't allow suspension per Mutex, so that we access a read-only copy of @running_actions.
39
+ # To avoid race conditions with thread creation, set a flag before the loop.
40
+ @shutdown = true
41
+
42
+ # terminate all running action threads
43
+ begin
44
+ @running_actions_for_gc.each(&:abort)
45
+ rescue ThreadError
46
+ # ThreadPool requires to lock a mutex, which fails in trap context.
47
+ # So defer the abort through another thread.
48
+ Thread.new do
49
+ @running_actions_for_gc.each(&:abort)
50
+ end
51
+ end
52
+
53
+ nil
54
+ end
55
+
56
+ def shutdown(&completion_block)
57
+ send_shutdown
58
+ if event_scope?
59
+ if completion_block
60
+ completion_block = new_async_proc(&completion_block)
61
+
62
+ # Thread might not be tagged to a calling event scope
63
+ source_event_loop = Thread.current.thread_variable_get(:__event_loop__)
64
+ Thread.current.thread_variable_set(:__event_loop__, nil)
65
+ begin
66
+ @threadpool.new do
67
+ @running_actions_for_gc.each(&:join)
68
+ completion_block.call
69
+ end
70
+ ensure
71
+ Thread.current.thread_variable_set(:__event_loop__, source_event_loop)
72
+ end
73
+ end
74
+ else
75
+ raise InvalidAccess, "external shutdown call doesn't take a block but blocks until threads have terminated" if completion_block
76
+ @running_actions_for_gc.each(&:join)
77
+ end
78
+ end
79
+
80
+ # Make a copy of the thread list for use in shutdown.
81
+ # The copy is replaced per an atomic operation, so that it can be read lock-free in shutdown.
82
+ def _update_action_threads_for_gc
83
+ @running_actions_for_gc = @running_actions.dup
84
+ end
85
+
86
+ # Is the caller running within the event scope context?
87
+ def event_scope?
88
+ @mutex.owned?
89
+ end
90
+
91
+ def synchronize_external
92
+ if event_scope?
93
+ yield
94
+ else
95
+ @mutex.synchronize do
96
+ yield
97
+ end
98
+ end
99
+ end
100
+
101
+ def with_call_frame(name, answer_queue)
102
+ source_event_loop = Thread.current.thread_variable_get(:__event_loop__)
103
+ @mutex.lock
104
+ begin
105
+ Thread.current.thread_variable_set(:__event_loop__, self)
106
+ @latest_answer_queue = answer_queue
107
+ @latest_call_name = name
108
+ start_time = Time.now
109
+ yield(source_event_loop)
110
+ ensure
111
+ @latest_answer_queue = nil
112
+ @latest_call_name = nil
113
+ @mutex.unlock
114
+ diff_time = Time.now - start_time
115
+ @guard_time_proc&.call(diff_time, name)
116
+ Thread.current.thread_variable_set(:__event_loop__, source_event_loop)
117
+ end
118
+ source_event_loop
119
+ end
120
+
121
+ def async_call(box, name, args, block, wrapper)
122
+ with_call_frame(name, nil) do |source_event_loop|
123
+ args = wrapper.call(source_event_loop, *args) if wrapper
124
+ args = Sanitizer.sanitize_values(args, source_event_loop, self, name)
125
+ block = Sanitizer.sanitize_value(block, source_event_loop, self, name)
126
+ box.send("__#{name}__", *args, &block)
127
+ end
128
+ end
129
+
130
+ def sync_call(box, name, args, block, answer_queue, wrapper)
131
+ with_call_frame(name, answer_queue) do |source_event_loop|
132
+ args = wrapper.call(source_event_loop, *args) if wrapper
133
+ args = Sanitizer.sanitize_values(args, source_event_loop, self, name)
134
+ block = Sanitizer.sanitize_value(block, source_event_loop, self, name)
135
+ res = box.send("__#{name}__", *args, &block)
136
+ res = Sanitizer.sanitize_value(res, self, source_event_loop)
137
+ answer_queue << res
138
+ end
139
+ end
140
+
141
+ def yield_call(box, name, args, kwargs, block, answer_queue, wrapper)
142
+ with_call_frame(name, answer_queue) do |source_event_loop|
143
+ args << _completion_proc(answer_queue, name, source_event_loop)
144
+ args << kwargs unless kwargs.empty?
145
+ args = wrapper.call(source_event_loop, *args) if wrapper
146
+ args = Sanitizer.sanitize_values(args, source_event_loop, self, name)
147
+ block = Sanitizer.sanitize_value(block, source_event_loop, self, name)
148
+ box.send("__#{name}__", *args, &block)
149
+ end
150
+ end
151
+
152
+ # Anonymous version of async_call
153
+ def async_proc_call(pr, args, arg_block, wrapper)
154
+ with_call_frame(AsyncProc, nil) do |source_event_loop|
155
+ args = wrapper.call(source_event_loop, *args) if wrapper
156
+ args = Sanitizer.sanitize_values(args, source_event_loop, self)
157
+ arg_block = Sanitizer.sanitize_value(arg_block, source_event_loop, self)
158
+ pr.yield(*args, &arg_block)
159
+ end
160
+ end
161
+
162
+ # Anonymous version of sync_call
163
+ def sync_proc_call(pr, args, arg_block, answer_queue, wrapper)
164
+ with_call_frame(SyncProc, answer_queue) do |source_event_loop|
165
+ args = wrapper.call(source_event_loop, *args) if wrapper
166
+ args = Sanitizer.sanitize_values(args, source_event_loop, self)
167
+ arg_block = Sanitizer.sanitize_value(arg_block, source_event_loop, self)
168
+ res = pr.yield(*args, &arg_block)
169
+ res = Sanitizer.sanitize_value(res, self, source_event_loop)
170
+ answer_queue << res
171
+ end
172
+ end
173
+
174
+ # Anonymous version of yield_call
175
+ def yield_proc_call(pr, args, kwargs, arg_block, answer_queue, wrapper)
176
+ with_call_frame(YieldProc, answer_queue) do |source_event_loop|
177
+ args << _completion_proc(answer_queue, pr, source_event_loop)
178
+ args << kwargs unless kwargs.empty?
179
+ args = wrapper.call(source_event_loop, *args) if wrapper
180
+ args = Sanitizer.sanitize_values(args, source_event_loop, self)
181
+ arg_block = Sanitizer.sanitize_value(arg_block, source_event_loop, self)
182
+ pr.yield(*args, &arg_block)
183
+ end
184
+ end
185
+
186
+ # Called when an external proc finished
187
+ def external_proc_result(cbresult, res)
188
+ with_call_frame(ExternalProc, nil) do
189
+ cbresult.yield(*res)
190
+ end
191
+ end
192
+
193
+ def new_async_proc(name=nil, klass=AsyncProc, &block)
194
+ raise InvalidAccess, "async_proc outside of the event scope is not allowed" unless event_scope?
195
+ wrapper = ArgumentWrapper.build(block, "async_proc #{name}")
196
+ pr = klass.new do |*args, &arg_block|
197
+ if event_scope?
198
+ # called in the event scope
199
+ block.yield(*args, &arg_block)
200
+ else
201
+ # called externally
202
+ async_proc_call(block, args, arg_block, wrapper)
203
+ end
204
+ pr
205
+ end
206
+ end
207
+
208
+ def new_sync_proc(name=nil, &block)
209
+ raise InvalidAccess, "sync_proc outside of the event scope is not allowed" unless event_scope?
210
+ wrapper = ArgumentWrapper.build(block, "sync_proc #{name}")
211
+ SyncProc.new do |*args, &arg_block|
212
+ if event_scope?
213
+ # called in the event scope
214
+ block.yield(*args, &arg_block)
215
+ else
216
+ # called externally
217
+ answer_queue = Queue.new
218
+ sel = sync_proc_call(block, args, arg_block, answer_queue, wrapper)
219
+ callback_loop(answer_queue, sel)
220
+ end
221
+ end
222
+ end
223
+
224
+ def new_yield_proc(name=nil, &block)
225
+ raise InvalidAccess, "yield_proc outside of the event scope is not allowed" unless event_scope?
226
+ wrapper = ArgumentWrapper.build(block, "yield_proc #{name}")
227
+ YieldProc.new do |*args, **kwargs, &arg_block|
228
+ if event_scope?
229
+ # called in the event scope
230
+ safe_yield_result(args, block)
231
+ args << kwargs unless kwargs.empty?
232
+ block.yield(*args, &arg_block)
233
+ nil
234
+ else
235
+ # called externally
236
+ answer_queue = Queue.new
237
+ sel = yield_proc_call(block, args, kwargs, arg_block, answer_queue, wrapper)
238
+ callback_loop(answer_queue, sel)
239
+ end
240
+ end
241
+ end
242
+
243
+ def safe_yield_result(args, name)
244
+ complete = args.last
245
+ unless Proc === complete
246
+ if Proc === name
247
+ raise InvalidAccess, "yield_proc #{name.inspect} must be called with a Proc object in the event scope but got #{complete.class}"
248
+ else
249
+ raise InvalidAccess, "yield_call `#{name}' must be called with a Proc object in the event scope but got #{complete.class}"
250
+ end
251
+ end
252
+ args[-1] = proc do |*cargs, &cblock|
253
+ unless complete
254
+ if Proc === name
255
+ raise MultipleResults, "received multiple results for #{name.inspect}"
256
+ else
257
+ raise MultipleResults, "received multiple results for method `#{name}'"
258
+ end
259
+ end
260
+ res = complete.yield(*cargs, &cblock)
261
+ complete = nil
262
+ res
263
+ end
264
+ end
265
+
266
+ private def _completion_proc(answer_queue, name, source_event_loop)
267
+ new_async_proc(name, CompletionProc) do |*resu|
268
+ unless answer_queue
269
+ if Proc === name
270
+ raise MultipleResults, "received multiple results for #{name.inspect}"
271
+ else
272
+ raise MultipleResults, "received multiple results for method `#{name}'"
273
+ end
274
+ end
275
+ resu = Sanitizer.sanitize_values(resu, self, source_event_loop)
276
+ resu = Sanitizer.return_args(resu)
277
+ answer_queue << resu
278
+ answer_queue = nil
279
+ end
280
+ end
281
+
282
+ def callback_loop(answer_queue, source_event_loop)
283
+ loop do
284
+ rets = answer_queue.deq
285
+ case rets
286
+ when Callback
287
+ cbres = rets.block.yield(*rets.args, &rets.arg_block)
288
+
289
+ if rets.cbresult
290
+ cbres = Sanitizer.sanitize_value(cbres, source_event_loop, self)
291
+ external_proc_result(rets.cbresult, cbres)
292
+ end
293
+ when WrappedException
294
+ answer_queue.close if answer_queue.respond_to?(:close)
295
+ raise(*rets.exc)
296
+ else
297
+ answer_queue.close if answer_queue.respond_to?(:close)
298
+ return rets
299
+ end
300
+ end
301
+ end
302
+
303
+ # Mark an object as to be shared instead of copied.
304
+ def shared_object(object)
305
+ if event_scope?
306
+ ObjectRegistry.set_tag(object, self)
307
+ else
308
+ ObjectRegistry.set_tag(object, ExternalSharedObject)
309
+ end
310
+ object
311
+ end
312
+
313
+ def thread_finished(action)
314
+ @mutex.synchronize do
315
+ @running_actions.delete(action) or raise(ArgumentError, "unknown action has finished: #{action}")
316
+ _update_action_threads_for_gc
317
+ end
318
+ end
319
+
320
+ Callback = Struct.new :block, :args, :arg_block, :cbresult
321
+
322
+ def _external_proc_call(block, name, args, arg_block, cbresult, source_event_loop)
323
+ if @latest_answer_queue
324
+ args = Sanitizer.sanitize_values(args, self, source_event_loop)
325
+ arg_block = Sanitizer.sanitize_value(arg_block, self, source_event_loop)
326
+ @latest_answer_queue << Callback.new(block, args, arg_block, cbresult)
327
+ nil
328
+ else
329
+ raise(InvalidAccess, "closure #{"defined by `#{name}' " if name}was yielded by `#{@latest_call_name}', which must a sync_call, yield_call, sync_proc or yield_proc")
330
+ end
331
+ end
332
+
333
+ def start_action(meth, name, args)
334
+ # Actions might not be tagged to a calling event scope
335
+ source_event_loop = Thread.current.thread_variable_get(:__event_loop__)
336
+ Thread.current.thread_variable_set(:__event_loop__, nil)
337
+
338
+ qu = Queue.new
339
+
340
+ new_thread = Thread.handle_interrupt(Exception => :never) do
341
+ @threadpool.new do
342
+ begin
343
+ Thread.handle_interrupt(AbortAction => :on_blocking) do
344
+ if meth.arity == args.length
345
+ meth.call(*args)
346
+ else
347
+ meth.call(*args, qu.deq)
348
+ end
349
+ end
350
+ rescue AbortAction
351
+ # Do nothing, just exit the action
352
+ rescue WeakRef::RefError
353
+ # It can happen that the GC already swept the Eventbox instance, before some instance action is in a blocking state.
354
+ # In this case access to the Eventbox instance raises a RefError.
355
+ # Since it's now impossible to execute the action up to a blocking state, abort the action prematurely.
356
+ raise unless @shutdown
357
+ ensure
358
+ thread_finished(qu.deq)
359
+ end
360
+ end
361
+ end
362
+
363
+ a = Action.new(name, new_thread, self)
364
+
365
+ # Add to the list of running actions
366
+ synchronize_external do
367
+ @running_actions << a
368
+ _update_action_threads_for_gc
369
+ end
370
+
371
+ # Enqueue the action twice (for call and for finish)
372
+ qu << a << a
373
+
374
+ # @shutdown is set without a lock, so that we need to re-check, if it was set while start_action
375
+ if @shutdown
376
+ a.abort
377
+ a.join
378
+ end
379
+
380
+ a
381
+ ensure
382
+ Thread.current.thread_variable_set(:__event_loop__, source_event_loop)
383
+ end
384
+ end
385
+ end