eventbox 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +1 -0
- data.tar.gz.sig +0 -0
- data/.appveyor.yml +28 -0
- data/.gitignore +8 -0
- data/.travis.yml +16 -0
- data/.yardopts +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +381 -0
- data/Rakefile +14 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/downloads.md +143 -0
- data/docs/server.md +88 -0
- data/docs/threadpool.md +73 -0
- data/eventbox.gemspec +31 -0
- data/lib/eventbox.rb +270 -0
- data/lib/eventbox/argument_wrapper.rb +76 -0
- data/lib/eventbox/boxable.rb +298 -0
- data/lib/eventbox/event_loop.rb +385 -0
- data/lib/eventbox/object_registry.rb +35 -0
- data/lib/eventbox/sanitizer.rb +342 -0
- data/lib/eventbox/thread_pool.rb +170 -0
- data/lib/eventbox/timer.rb +172 -0
- data/lib/eventbox/version.rb +5 -0
- metadata +156 -0
- metadata.gz.sig +0 -0
@@ -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
|