eventbox 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
1
+ # frozen-string-literal: true
2
+
3
+ class Eventbox
4
+ class ObjectRegistry
5
+ class << self
6
+ def taggable?(object)
7
+ case object
8
+ # Keep the list of non taggable object types in sync with Sanitizer.sanitize_value
9
+ when NilClass, Numeric, Symbol, TrueClass, FalseClass, WrappedObject, Action, Module, Eventbox, Proc
10
+ false
11
+ else
12
+ if object.frozen?
13
+ false
14
+ else
15
+ true
16
+ end
17
+ end
18
+ end
19
+
20
+ def set_tag(object, new_tag)
21
+ raise InvalidAccess, "object is not taggable: #{object.inspect}" unless taggable?(object)
22
+
23
+ tag = get_tag(object)
24
+ if tag && tag != new_tag
25
+ raise InvalidAccess, "object #{object.inspect} is already tagged to #{tag.inspect}"
26
+ end
27
+ object.instance_variable_set(:@__event_box_tag__, new_tag)
28
+ end
29
+
30
+ def get_tag(object)
31
+ object.instance_variable_defined?(:@__event_box_tag__) && object.instance_variable_get(:@__event_box_tag__)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,342 @@
1
+ # frozen-string-literal: true
2
+
3
+ class Eventbox
4
+ # Module for argument and result value sanitation.
5
+ #
6
+ # All call arguments and result values between external and event scope an vice versa are passed through the Sanitizer.
7
+ # This filter is required to prevent data races through shared objects or non-synchonized proc execution.
8
+ # It also wraps blocks and Proc objects to arbitrate between external blocking behaviour and internal event based behaviour.
9
+ #
10
+ # Depending on the type of the object and the direction of the call it is passed
11
+ # * directly (immutable object types or already wrapped objects)
12
+ # * as a deep copy (if copyable)
13
+ # * as a safely callable wrapped object (Proc objects)
14
+ # * as a non-callable wrapped object (non copyable objects)
15
+ # * as an unwrapped object (when passing a wrapped object back to origin scope)
16
+ #
17
+ # The filter is recursively applied to all object data (instance variables or elements), if the object is non copyable.
18
+ #
19
+ # In detail this works as following.
20
+ # Objects which are passed through unchanged are:
21
+ # * {Eventbox}, {Eventbox::Action} and `Module` objects
22
+ # * Proc objects created by {Eventbox#async_proc}, {Eventbox#sync_proc} and {Eventbox#yield_proc}
23
+ #
24
+ # The following rules apply for wrapping/unwrapping:
25
+ # * If the object has been marked as {Eventbox#shared_object}, it is wrapped as {WrappedObject} depending on the direction of the data flow (return value or call argument).
26
+ # * If the object is a {WrappedObject} or {ExternalProc} and fits to the target scope, it is unwrapped.
27
+ # Both cases even work if the object is encapsulated by another object.
28
+ #
29
+ # In all other cases the following rules apply:
30
+ # * If the object is marshalable, it is passed as a deep copy through `Marshal.dump` and `Marshal.load` .
31
+ # * An object which failed to marshal as a whole is tried to be dissected and values are sanitized recursively.
32
+ # * If the object can't be marshaled or dissected, it is wrapped as {WrappedObject}.
33
+ # They are unwrapped when passed back to origin scope.
34
+ # * Proc objects passed from event scope to external are wrapped as {WrappedObject}.
35
+ # They are unwrapped when passed back to event scope.
36
+ # * Proc objects passed from external to event scope are wrapped as {ExternalProc}.
37
+ # They are unwrapped when passed back to external scope.
38
+ module Sanitizer
39
+ module_function
40
+
41
+ def return_args(args)
42
+ args.length <= 1 ? args.first : args
43
+ end
44
+
45
+ def dissect_instance_variables(arg, source_event_loop, target_event_loop)
46
+ # Separate the instance variables from the object
47
+ ivns = arg.instance_variables
48
+ ivvs = ivns.map do |ivn|
49
+ arg.instance_variable_get(ivn)
50
+ end
51
+
52
+ # Temporary set all instance variables to nil
53
+ ivns.each do |ivn|
54
+ arg.instance_variable_set(ivn, nil)
55
+ end
56
+
57
+ # Copy the object
58
+ arg2 = yield(arg)
59
+
60
+ # Restore the original object
61
+ ivns.each_with_index do |ivn, ivni|
62
+ arg.instance_variable_set(ivn, ivvs[ivni])
63
+ end
64
+
65
+ # sanitize instance variables independently and write them to the copied object
66
+ ivns.each_with_index do |ivn, ivni|
67
+ ivv = sanitize_value(ivvs[ivni], source_event_loop, target_event_loop, ivn)
68
+ arg2.instance_variable_set(ivn, ivv)
69
+ end
70
+
71
+ arg2
72
+ end
73
+
74
+ def dissect_struct_members(arg, source_event_loop, target_event_loop)
75
+ ms = arg.members
76
+ # call Array#map on Struct#values to work around bug JRuby bug https://github.com/jruby/jruby/issues/5372
77
+ vs = arg.values.map{|a| a }
78
+
79
+ ms.each do |m|
80
+ arg[m] = nil
81
+ end
82
+
83
+ arg2 = yield(arg)
84
+
85
+ ms.each_with_index do |m, i|
86
+ arg[m] = vs[i]
87
+ end
88
+
89
+ ms.each_with_index do |m, i|
90
+ v2 = sanitize_value(vs[i], source_event_loop, target_event_loop, m)
91
+ arg2[m] = v2
92
+ end
93
+
94
+ arg2
95
+ end
96
+
97
+ def dissect_hash_values(arg, source_event_loop, target_event_loop)
98
+ h = arg.dup
99
+
100
+ h.each_key do |k|
101
+ arg[k] = nil
102
+ end
103
+
104
+ arg2 = yield(arg)
105
+
106
+ h.each do |k, v|
107
+ arg[k] = v
108
+ end
109
+
110
+ h.each do |k, v|
111
+ arg2[k] = sanitize_value(v, source_event_loop, target_event_loop, k)
112
+ end
113
+
114
+ arg2
115
+ end
116
+
117
+ def dissect_array_values(arg, source_event_loop, target_event_loop, name)
118
+ vs = arg.dup
119
+
120
+ vs.each_index do |i|
121
+ arg[i] = nil
122
+ end
123
+
124
+ arg2 = yield(arg)
125
+
126
+ vs.each_index do |i|
127
+ arg[i] = vs[i]
128
+ end
129
+
130
+ vs.each_with_index do |v, i|
131
+ v2 = sanitize_value(v, source_event_loop, target_event_loop, name)
132
+ arg2[i] = v2
133
+ end
134
+
135
+ arg2
136
+ end
137
+
138
+ def sanitize_value(arg, source_event_loop, target_event_loop, name=nil)
139
+ case arg
140
+ when NilClass, Numeric, Symbol, TrueClass, FalseClass # Immutable objects
141
+ arg
142
+ when WrappedObject
143
+ arg.object_for(target_event_loop)
144
+ when ExternalProc
145
+ arg.object_for(target_event_loop)
146
+ when InternalProc, Action # If object is already wrapped -> pass it through
147
+ arg
148
+ when Module # Class or Module definitions are passed through
149
+ arg
150
+ when Eventbox # Eventbox objects already sanitize all inputs and outputs and are thread safe
151
+ arg
152
+ when Proc
153
+ wrap_proc(arg, name, source_event_loop, target_event_loop)
154
+ else
155
+ # Check if the object has been tagged
156
+ case mel=ObjectRegistry.get_tag(arg)
157
+ when EventLoop # Event scope object marked as shared_object
158
+ unless mel == source_event_loop
159
+ raise InvalidAccess, "object #{arg.inspect} #{"wrapped by #{name} " if name} was marked as shared_object in a different eventbox object than the calling eventbox"
160
+ end
161
+ WrappedObject.new(arg, mel, name)
162
+ when ExternalSharedObject # External object marked as shared_object
163
+ WrappedObject.new(arg, source_event_loop, name)
164
+ else
165
+ # Not tagged -> try to deep copy the object
166
+ begin
167
+ dumped = Marshal.dump(arg)
168
+ rescue TypeError
169
+
170
+ # Try to separate internal data from the object to sanitize it independently
171
+ begin
172
+ case arg
173
+ when Array
174
+ dissect_array_values(arg, source_event_loop, target_event_loop, name) do |arg2|
175
+ dissect_instance_variables(arg2, source_event_loop, target_event_loop) do |arg3|
176
+ Marshal.load(Marshal.dump(arg3))
177
+ end
178
+ end
179
+
180
+ when Hash
181
+ dissect_hash_values(arg, source_event_loop, target_event_loop) do |arg2|
182
+ dissect_instance_variables(arg2, source_event_loop, target_event_loop) do |arg3|
183
+ Marshal.load(Marshal.dump(arg3))
184
+ end
185
+ end
186
+
187
+ when Struct
188
+ dissect_struct_members(arg, source_event_loop, target_event_loop) do |arg2|
189
+ dissect_instance_variables(arg2, source_event_loop, target_event_loop) do |arg3|
190
+ Marshal.load(Marshal.dump(arg3))
191
+ end
192
+ end
193
+
194
+ else
195
+ dissect_instance_variables(arg, source_event_loop, target_event_loop) do |empty_arg|
196
+ # Retry to dump the now empty object
197
+ Marshal.load(Marshal.dump(empty_arg))
198
+ end
199
+ end
200
+ rescue TypeError
201
+ if source_event_loop
202
+ ObjectRegistry.set_tag(arg, source_event_loop)
203
+ else
204
+ ObjectRegistry.set_tag(arg, ExternalSharedObject)
205
+ end
206
+
207
+ # Object not copyable -> wrap object as event scope or external object
208
+ sanitize_value(arg, source_event_loop, target_event_loop, name)
209
+ end
210
+
211
+ else
212
+ Marshal.load(dumped)
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ def sanitize_values(args, source_event_loop, target_event_loop, name=nil)
219
+ args.map { |arg| sanitize_value(arg, source_event_loop, target_event_loop, name) }
220
+ end
221
+
222
+ def wrap_proc(arg, name, source_event_loop, target_event_loop)
223
+ if target_event_loop&.event_scope?
224
+ ExternalProc.new(arg, source_event_loop, name) do |*args, &block|
225
+ if target_event_loop&.event_scope?
226
+ # called in the event scope
227
+ if block && !(WrappedProc === block)
228
+ raise InvalidAccess, "calling #{arg.inspect} with block argument #{block.inspect} is not allowed - use async_proc, sync_proc, yield_proc or an external proc instead"
229
+ end
230
+ cbblock = args.last if Proc === args.last
231
+ target_event_loop._external_proc_call(arg, name, args, block, cbblock, source_event_loop)
232
+ else
233
+ # called externally
234
+ raise InvalidAccess, "external proc #{arg.inspect} #{"wrapped by #{name} " if name} can not be called in a different eventbox instance"
235
+ end
236
+ end
237
+ else
238
+ WrappedObject.new(arg, source_event_loop, name)
239
+ end
240
+ end
241
+ end
242
+
243
+ # Generic wrapper for objects that are passed through a foreign scope as reference.
244
+ #
245
+ # Access to the object from a different scope is denied, but the wrapper object can be stored and passed back to the origin scope to unwrap it.
246
+ class WrappedObject
247
+ attr_reader :name
248
+ def initialize(object, event_loop, name=nil)
249
+ @object = object
250
+ @event_loop = event_loop
251
+ @name = name
252
+ @dont_marshal = ExternalSharedObject # protect self from being marshaled
253
+ end
254
+
255
+ def object_for(target_event_loop)
256
+ @event_loop == target_event_loop ? @object : self
257
+ end
258
+
259
+ def inspect
260
+ "#<#{self.class} @object=#{@object.inspect} @name=#{@name.inspect}>"
261
+ end
262
+ end
263
+
264
+ # Base class for Proc objects created in any scope.
265
+ class WrappedProc < Proc
266
+ end
267
+
268
+ # Base class for Proc objects created in the event scope of some Eventbox instance.
269
+ class InternalProc < WrappedProc
270
+ end
271
+
272
+ # Proc objects created in the event scope of some Eventbox instance per {Eventbox#async_proc}
273
+ class AsyncProc < InternalProc
274
+ end
275
+
276
+ # Proc objects created in the event scope of some Eventbox instance per {Eventbox#sync_proc}
277
+ class SyncProc < InternalProc
278
+ end
279
+
280
+ # Proc objects created in the event scope of some Eventbox instance per {Eventbox#yield_proc}
281
+ class YieldProc < InternalProc
282
+ end
283
+
284
+ WrappedException = Struct.new(:exc)
285
+
286
+ # Proc object provided as the last argument of {Eventbox.yield_call} and {Eventbox#yield_proc}.
287
+ class CompletionProc < AsyncProc
288
+ # Raise an exception in the context of the waiting {Eventbox.yield_call} or {Eventbox#yield_proc} method.
289
+ #
290
+ # This allows to raise an exception to the calling scope from external or action scope:
291
+ #
292
+ # class MyBox < Eventbox
293
+ # yield_call def init(result)
294
+ # process(result)
295
+ # end
296
+ #
297
+ # action def process(result)
298
+ # result.raise RuntimeError, "raise from action MyBox#process"
299
+ # end
300
+ # end
301
+ # MyBox.new # => raises RuntimeError (raise from action MyBox#process)
302
+ #
303
+ # In contrast to a direct call of `Kernel.raise`, calling this method doesn't abort the current context.
304
+ # Instead when in the event scope, raising the exception is deferred until returning to the calling external or action scope.
305
+ def raise(*args)
306
+ self.call(WrappedException.new(args))
307
+ end
308
+ end
309
+
310
+ # Wrapper for Proc objects created external of some Eventbox instance.
311
+ #
312
+ # External Proc objects can be invoked from event scope through {Eventbox.sync_call} and {Eventbox.yield_call} methods.
313
+ # Optionally a proc can be provided as the last argument which acts as a completion callback.
314
+ # This proc is invoked, when the call has finished, with the result value as argument.
315
+ #
316
+ # class Callback < Eventbox
317
+ # sync_call def init(&block)
318
+ # block.call(5, proc do |res| # invoke the block given to Callback.new
319
+ # p res # print the block result (5 + 1)
320
+ # end)
321
+ # end
322
+ # end
323
+ # Callback.new {|num| num + 1 } # Output: 6
324
+ #
325
+ # External Proc objects can also be passed to action or to external scope.
326
+ # In this case a {ExternalProc} is unwrapped back to an ordinary Proc object.
327
+ class ExternalProc < WrappedProc
328
+ attr_reader :name
329
+ def initialize(object, event_loop, name=nil)
330
+ @object = object
331
+ @event_loop = event_loop
332
+ @name = name
333
+ end
334
+
335
+ def object_for(target_event_loop)
336
+ @event_loop == target_event_loop ? @object : self
337
+ end
338
+ end
339
+
340
+ # @private
341
+ ExternalSharedObject = IO.pipe.first
342
+ end
@@ -0,0 +1,170 @@
1
+ # frozen-string-literal: true
2
+
3
+ class Eventbox
4
+ # A pool of reusable threads for actions
5
+ #
6
+ # By default each call of an action method spawns a new thread and terminates the thread when the action is finished.
7
+ # If there are many short lived action calls, creation and termination of threads can be a bottleneck.
8
+ # In this case it is desireable to reuse threads for multiple actions.
9
+ # This is what a threadpool is made for.
10
+ #
11
+ # A threadpool creates a fixed number of threads at startup and distributes all action calls to free threads.
12
+ # If no free thread is available, the request in enqueued and processed in order.
13
+ #
14
+ # It is possible to use one threadpool for several {Eventbox} derivations and {Eventbox} instances at the same time.
15
+ # However using a threadpool adds the risk of deadlocks, if actions depend of each other and the threadpool provides too less threads.
16
+ # A threadpool can slow actions down, if too less threads are allocated, so that actions are enqueued.
17
+ # On the other hand a threadpool can also slow processing down, if the threadpool allocates many threads at startup, but doesn't makes use of them.
18
+ #
19
+ # An Eventbox with associated {ThreadPool} can be created per {Eventbox.with_options}.
20
+ # +num_threads+ is the number of allocated threads:
21
+ # EventboxWithThreadpool = Eventbox.with_options(threadpool: Eventbox::ThreadPool.new(num_threads))
22
+ class ThreadPool < Eventbox
23
+ class AbortAction < RuntimeError; end
24
+
25
+ # Representation of a work task given as block to ThreadPool.new
26
+ class PoolThread < Eventbox
27
+ # It has 3 implicit states: enqueued, running, finished
28
+ # Variables for state "enqueued": @block, @joins, @signals
29
+ # Variables for state "running": @joins, @action
30
+ # Variables for state "finished": -
31
+ # Variables unused in this state are set to `nil`.
32
+ async_call def init(block, action)
33
+ @block = block
34
+ @joins = []
35
+ @signals = block ? [] : nil
36
+ @action = action
37
+ end
38
+
39
+ async_call def raise(*args)
40
+ # Eventbox::AbortAction would shutdown the thread pool.
41
+ # To stop the borrowed thread only remap to Eventbox::ThreadPool::AbortAction .
42
+ args[0] = AbortAction if args[0] == Eventbox::AbortAction
43
+
44
+ if a=@action
45
+ # The task is running -> send the signal to the thread
46
+ a.raise(*args)
47
+ elsif s=@signals
48
+ # The task is still enqueued -> add the signal to the request
49
+ s << args
50
+ end
51
+ end
52
+
53
+ # Belongs the current thread to this action.
54
+ sync_call def current?
55
+ if a=@action
56
+ a.current?
57
+ else
58
+ false
59
+ end
60
+ end
61
+
62
+ yield_call def join(result)
63
+ if j=@joins
64
+ j << result
65
+ else
66
+ # action has already finished
67
+ result.yield
68
+ end
69
+ end
70
+
71
+ # @private
72
+ async_call def __start__(action, input)
73
+ # Send the block to the start_pool_thread as result of next_job
74
+ input.yield(self, @block)
75
+
76
+ # Send all accumulated signals to the action thread
77
+ @signals.each do |sig|
78
+ action.raise(*sig)
79
+ end
80
+
81
+ @action = action
82
+ @signals = nil
83
+ @block = nil
84
+ end
85
+
86
+ # @private
87
+ async_call def __finish__
88
+ @action = nil
89
+ @joins.each(&:yield)
90
+ @joins = nil
91
+ end
92
+ end
93
+
94
+ async_call def init(pool_size, run_gc_when_busy: false)
95
+ @jobless = []
96
+ @requests = []
97
+ @run_gc_when_busy = run_gc_when_busy
98
+
99
+ pool_size.times do
100
+ start_pool_thread
101
+ end
102
+ end
103
+
104
+ action def start_pool_thread(action)
105
+ while true
106
+ req, bl = next_job(action)
107
+ begin
108
+ Thread.handle_interrupt(AbortAction => :on_blocking) do
109
+ bl.yield
110
+ end
111
+ rescue AbortAction
112
+ # The pooled action was aborted, but the thread keeps going
113
+ ensure
114
+ req.__finish__
115
+ end
116
+
117
+ # Discard all interrupts which are too late to arrive the running action
118
+ while Thread.pending_interrupt?
119
+ begin
120
+ Thread.handle_interrupt(Exception => :immediate) do
121
+ sleep # Aborted by the exception
122
+ end
123
+ rescue Exception
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ private yield_call def next_job(action, input)
130
+ if @requests.empty?
131
+ @jobless << [action, input]
132
+ else
133
+ # Take the oldest request and send it to the calling action.
134
+ req = @requests.shift
135
+ req.__start__(action, input)
136
+ end
137
+ end
138
+
139
+ sync_call def new(&block)
140
+ if @jobless.empty?
141
+ # No free thread -> enqueue the request
142
+ req = PoolThread.new(block, nil)
143
+ @requests << req
144
+
145
+ # Try to release some actions by the GC
146
+ if @run_gc_when_busy
147
+ @run_gc_when_busy = false # Start only one GC run
148
+ gc_start
149
+ end
150
+ else
151
+ # Immediately start the block
152
+ action, input = @jobless.shift
153
+ req = PoolThread.new(nil, action)
154
+ input.yield(req, block)
155
+ end
156
+
157
+ req
158
+ end
159
+
160
+ private action def gc_start
161
+ GC.start
162
+ ensure
163
+ gc_finished
164
+ end
165
+
166
+ private async_call def gc_finished
167
+ @run_gc_when_busy = true
168
+ end
169
+ end
170
+ end