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,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