ractor-wrapper 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/LICENSE.md +1 -1
- data/README.md +163 -93
- data/lib/ractor/wrapper/version.rb +3 -1
- data/lib/ractor/wrapper.rb +860 -251
- data/lib/ractor-wrapper.rb +2 -0
- metadata +11 -10
data/lib/ractor/wrapper.rb
CHANGED
|
@@ -1,166 +1,387 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
##
|
|
2
4
|
# See ruby-doc.org for info on Ractors.
|
|
3
5
|
#
|
|
4
6
|
class Ractor
|
|
5
7
|
##
|
|
6
|
-
# An experimental class that wraps a non-shareable object
|
|
7
|
-
# Ractors to access it concurrently.
|
|
8
|
+
# An experimental class that wraps a non-shareable object in an actor,
|
|
9
|
+
# allowing multiple Ractors to access it concurrently.
|
|
10
|
+
#
|
|
11
|
+
# WARNING: This is a highly experimental library, and currently _not_
|
|
12
|
+
# recommended for production use. (As of Ruby 4.0.0, the same can be said of
|
|
13
|
+
# Ractors in general.)
|
|
8
14
|
#
|
|
9
15
|
# ## What is Ractor::Wrapper?
|
|
10
16
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# resource that is stateful
|
|
15
|
-
#
|
|
17
|
+
# For the most part, unless an object is _sharable_, which generally means
|
|
18
|
+
# deeply immutable along with a few other restrictions, it cannot be accessed
|
|
19
|
+
# directly from another Ractor. This makes it difficult for multiple Ractors
|
|
20
|
+
# to share a resource that is stateful. Such a resource must typically itself
|
|
21
|
+
# be implemented as a Ractor and accessed via message passing.
|
|
22
|
+
#
|
|
23
|
+
# Ractor::Wrapper makes it possible for an ordinary non-shareable object to
|
|
24
|
+
# be accessed from multiple Ractors. It does this by "wrapping" the object
|
|
25
|
+
# with an actor that listens for messages and invokes the object's methods in
|
|
26
|
+
# a controlled single-Ractor environment. It then provides a stub object that
|
|
27
|
+
# reproduces the interface of the original object, but responds to method
|
|
28
|
+
# calls by sending messages to the wrapper. Ractor::Wrapper can be used to
|
|
29
|
+
# implement simple actors by writing "plain" Ruby objects, or to adapt
|
|
30
|
+
# existing non-shareable objects to a multi-Ractor world.
|
|
31
|
+
#
|
|
32
|
+
# ## Net::HTTP example
|
|
33
|
+
#
|
|
34
|
+
# The following example shows how to share a single Net::HTTP session object
|
|
35
|
+
# among multiple Ractors.
|
|
36
|
+
#
|
|
37
|
+
# require "ractor/wrapper"
|
|
38
|
+
# require "net/http"
|
|
39
|
+
#
|
|
40
|
+
# # Create a Net::HTTP session. Net::HTTP sessions are not shareable,
|
|
41
|
+
# # so normally only one Ractor can access them at a time.
|
|
42
|
+
# http = Net::HTTP.new("example.com")
|
|
43
|
+
# http.start
|
|
44
|
+
#
|
|
45
|
+
# # Create a wrapper around the session. This moves the session into an
|
|
46
|
+
# # internal Ractor and listens for method call requests. By default, a
|
|
47
|
+
# # wrapper serializes calls, handling one at a time, for compatibility
|
|
48
|
+
# # with non-thread-safe objects.
|
|
49
|
+
# wrapper = Ractor::Wrapper.new(http)
|
|
50
|
+
#
|
|
51
|
+
# # At this point, the session object can no longer be accessed directly
|
|
52
|
+
# # because it is now owned by the wrapper's internal Ractor.
|
|
53
|
+
# # http.get("/whoops") # <= raises Ractor::MovedError
|
|
54
|
+
#
|
|
55
|
+
# # However, you can access the session via the stub object provided by
|
|
56
|
+
# # the wrapper. This stub proxies the call to the wrapper's internal
|
|
57
|
+
# # Ractor. And it's shareable, so any number of Ractors can use it.
|
|
58
|
+
# response = wrapper.stub.get("/")
|
|
59
|
+
#
|
|
60
|
+
# # Here, we start two Ractors, and pass the stub to each one. Each
|
|
61
|
+
# # Ractor can simply call methods on the stub as if it were the original
|
|
62
|
+
# # connection object. Internally, of course, the calls are proxied to
|
|
63
|
+
# # the original object via the wrapper, and execution is serialized.
|
|
64
|
+
# r1 = Ractor.new(wrapper.stub) do |stub|
|
|
65
|
+
# 5.times do
|
|
66
|
+
# stub.get("/hello")
|
|
67
|
+
# end
|
|
68
|
+
# :ok
|
|
69
|
+
# end
|
|
70
|
+
# r2 = Ractor.new(wrapper.stub) do |stub|
|
|
71
|
+
# 5.times do
|
|
72
|
+
# stub.get("/ruby")
|
|
73
|
+
# end
|
|
74
|
+
# :ok
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
# # Wait for the two above Ractors to finish.
|
|
78
|
+
# r1.join
|
|
79
|
+
# r2.join
|
|
80
|
+
#
|
|
81
|
+
# # After you stop the wrapper, you can retrieve the underlying session
|
|
82
|
+
# # object and access it directly again.
|
|
83
|
+
# wrapper.async_stop
|
|
84
|
+
# http = wrapper.recover_object
|
|
85
|
+
# http.finish
|
|
16
86
|
#
|
|
17
|
-
#
|
|
18
|
-
# implemented as an object and accessed using ordinary method calls. It does
|
|
19
|
-
# this by "wrapping" the object in a Ractor, and mapping method calls to
|
|
20
|
-
# message passing. This may make it easier to implement such a resource with
|
|
21
|
-
# a simple class rather than a full-blown Ractor with message passing, and it
|
|
22
|
-
# may also useful for adapting existing legacy object-based implementations.
|
|
87
|
+
# ## SQLite3 example
|
|
23
88
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# on which you can invoke methods. The wrapper responds to these method calls
|
|
27
|
-
# by sending messages to the internal Ractor, which invokes the shared object
|
|
28
|
-
# and then sends back the result. If the underlying object is thread-safe,
|
|
29
|
-
# you can configure the wrapper to run multiple threads that can run methods
|
|
30
|
-
# concurrently. Or, if not, the wrapper can serialize requests to the object.
|
|
89
|
+
# The following example shows how to share a SQLite3 database among multiple
|
|
90
|
+
# Ractors.
|
|
31
91
|
#
|
|
32
|
-
#
|
|
92
|
+
# require "ractor/wrapper"
|
|
93
|
+
# require "sqlite3"
|
|
33
94
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
95
|
+
# # Create a SQLite3 database. These objects are not shareable, so
|
|
96
|
+
# # normally only one Ractor can access them.
|
|
97
|
+
# db = SQLite3::Database.new($my_database_path)
|
|
37
98
|
#
|
|
38
|
-
#
|
|
99
|
+
# # Create a wrapper around the database. A SQLite3::Database object
|
|
100
|
+
# # cannot be moved between Ractors, so we configure the wrapper to run
|
|
101
|
+
# # in the current Ractor. You can also configure it to run multiple
|
|
102
|
+
# # worker threads because the database object itself is thread-safe.
|
|
103
|
+
# wrapper = Ractor::Wrapper.new(db, use_current_ractor: true, threads: 2)
|
|
39
104
|
#
|
|
40
|
-
# #
|
|
41
|
-
#
|
|
42
|
-
#
|
|
105
|
+
# # At this point, the database object can still be accessed directly
|
|
106
|
+
# # because it hasn't been moved to a different Ractor.
|
|
107
|
+
# rows = db.execute("select * from numbers")
|
|
43
108
|
#
|
|
44
|
-
# #
|
|
45
|
-
# #
|
|
46
|
-
#
|
|
109
|
+
# # You can also access the database via the stub object provided by the
|
|
110
|
+
# # wrapper.
|
|
111
|
+
# rows = wrapper.stub.execute("select * from numbers")
|
|
47
112
|
#
|
|
48
|
-
# #
|
|
49
|
-
# #
|
|
50
|
-
# #
|
|
51
|
-
# r1 = Ractor.new(wrapper) do |
|
|
52
|
-
#
|
|
53
|
-
#
|
|
113
|
+
# # Here, we start two Ractors, and pass the stub to each one. The
|
|
114
|
+
# # wrapper's two worker threads will handle the requests in the order
|
|
115
|
+
# # received.
|
|
116
|
+
# r1 = Ractor.new(wrapper.stub) do |db_stub|
|
|
117
|
+
# 5.times do
|
|
118
|
+
# rows = db_stub.execute("select * from numbers")
|
|
54
119
|
# end
|
|
55
120
|
# :ok
|
|
56
121
|
# end
|
|
57
|
-
# r2 = Ractor.new(wrapper) do |
|
|
58
|
-
#
|
|
59
|
-
#
|
|
122
|
+
# r2 = Ractor.new(wrapper.stub) do |db_stub|
|
|
123
|
+
# 5.times do
|
|
124
|
+
# rows = db_stub.execute("select * from numbers")
|
|
60
125
|
# end
|
|
61
126
|
# :ok
|
|
62
127
|
# end
|
|
63
128
|
#
|
|
64
129
|
# # Wait for the two above Ractors to finish.
|
|
65
|
-
# r1.
|
|
66
|
-
# r2.
|
|
130
|
+
# r1.join
|
|
131
|
+
# r2.join
|
|
67
132
|
#
|
|
68
|
-
# # After
|
|
69
|
-
# #
|
|
133
|
+
# # After stopping the wrapper, you can call the join method to wait for
|
|
134
|
+
# # it to completely finish.
|
|
70
135
|
# wrapper.async_stop
|
|
71
|
-
#
|
|
72
|
-
#
|
|
136
|
+
# wrapper.join
|
|
137
|
+
#
|
|
138
|
+
# # When running a wrapper with :use_current_ractor, you do not need to
|
|
139
|
+
# # recover the object, because it was never moved. The recover_object
|
|
140
|
+
# # method is not available.
|
|
141
|
+
# # db2 = wrapper.recover_object # <= raises Ractor::Error
|
|
73
142
|
#
|
|
74
143
|
# ## Features
|
|
75
144
|
#
|
|
76
|
-
# * Provides a method interface to
|
|
145
|
+
# * Provides a Ractor-shareable method interface to a non-shareable object.
|
|
77
146
|
# * Supports arbitrary method arguments and return values.
|
|
78
|
-
# *
|
|
79
|
-
#
|
|
80
|
-
#
|
|
147
|
+
# * Can be configured to run in its own isolated Ractor or in a Thread in
|
|
148
|
+
# the current Ractor.
|
|
149
|
+
# * Can be configured per method whether to copy or move arguments and
|
|
150
|
+
# return values.
|
|
151
|
+
# * Blocks can be run in the calling Ractor or in the object Ractor.
|
|
152
|
+
# * Raises exceptions thrown by the method.
|
|
153
|
+
# * Can serialize method calls for non-thread-safe objects, or run methods
|
|
154
|
+
# concurrently in multiple worker threads for thread-safe objects.
|
|
81
155
|
# * Can gracefully shut down the wrapper and retrieve the original object.
|
|
82
156
|
#
|
|
83
157
|
# ## Caveats
|
|
84
158
|
#
|
|
85
|
-
# Ractor::Wrapper is subject to some limitations (and bugs) of Ractors, as of
|
|
86
|
-
# Ruby 3.0.0.
|
|
87
|
-
#
|
|
88
|
-
# * You cannot pass blocks to wrapped methods.
|
|
89
159
|
# * Certain types cannot be used as method arguments or return values
|
|
90
|
-
# because
|
|
91
|
-
# include threads,
|
|
92
|
-
# *
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
# *
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
160
|
+
# because they cannot be moved between Ractors. As of Ruby 4.0.0, these
|
|
161
|
+
# include threads, backtraces, procs, and a few others.
|
|
162
|
+
# * As of Ruby 4.0.0, any exceptions raised are always copied (rather than
|
|
163
|
+
# moved) back to the calling Ractor, and the backtrace is cleared out.
|
|
164
|
+
# This is due to https://bugs.ruby-lang.org/issues/21818
|
|
165
|
+
# * Blocks can be run "in place" (i.e. in the wrapped object context) only
|
|
166
|
+
# if the block does not access any data outside the block. Otherwise, the
|
|
167
|
+
# block must be run in caller's context.
|
|
168
|
+
# * Blocks configured to run in the caller's context can only be run while
|
|
169
|
+
# a method is executing. They cannot be "saved" as a proc to be run
|
|
170
|
+
# later unless they are configured to run "in place". In particular,
|
|
171
|
+
# using blocks as a syntax to define callbacks can generally not be done
|
|
172
|
+
# through a wrapper.
|
|
99
173
|
#
|
|
100
174
|
class Wrapper
|
|
175
|
+
##
|
|
176
|
+
# A stub that forwards calls to a wrapper.
|
|
177
|
+
#
|
|
178
|
+
# This object is shareable and can be passed to any Ractor.
|
|
179
|
+
#
|
|
180
|
+
class Stub
|
|
181
|
+
##
|
|
182
|
+
# Create a stub given a wrapper.
|
|
183
|
+
#
|
|
184
|
+
# @param wrapper [Ractor::Wrapper]
|
|
185
|
+
#
|
|
186
|
+
def initialize(wrapper)
|
|
187
|
+
@wrapper = wrapper
|
|
188
|
+
freeze
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
##
|
|
192
|
+
# Forward calls to {Ractor::Wrapper#call}.
|
|
193
|
+
# @private
|
|
194
|
+
#
|
|
195
|
+
def method_missing(name, ...)
|
|
196
|
+
@wrapper.call(name, ...)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
##
|
|
200
|
+
# Forward respond_to queries.
|
|
201
|
+
# @private
|
|
202
|
+
#
|
|
203
|
+
def respond_to_missing?(name, include_all)
|
|
204
|
+
@wrapper.call(:respond_to?, name, include_all)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
##
|
|
209
|
+
# Settings for a method call. Specifies how a method's arguments and
|
|
210
|
+
# return value are communicated (i.e. copy or move semantics.)
|
|
211
|
+
#
|
|
212
|
+
class MethodSettings
|
|
213
|
+
# @private
|
|
214
|
+
def initialize(move_data: false,
|
|
215
|
+
move_arguments: nil,
|
|
216
|
+
move_results: nil,
|
|
217
|
+
move_block_arguments: nil,
|
|
218
|
+
move_block_results: nil,
|
|
219
|
+
execute_blocks_in_place: nil)
|
|
220
|
+
@move_arguments = interpret_setting(move_arguments, move_data)
|
|
221
|
+
@move_results = interpret_setting(move_results, move_data)
|
|
222
|
+
@move_block_arguments = interpret_setting(move_block_arguments, move_data)
|
|
223
|
+
@move_block_results = interpret_setting(move_block_results, move_data)
|
|
224
|
+
@execute_blocks_in_place = interpret_setting(execute_blocks_in_place, false)
|
|
225
|
+
freeze
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
##
|
|
229
|
+
# @return [Boolean] Whether to move arguments
|
|
230
|
+
#
|
|
231
|
+
def move_arguments?
|
|
232
|
+
@move_arguments
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
##
|
|
236
|
+
# @return [Boolean] Whether to move return values
|
|
237
|
+
#
|
|
238
|
+
def move_results?
|
|
239
|
+
@move_results
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
##
|
|
243
|
+
# @return [Boolean] Whether to move arguments to a block
|
|
244
|
+
#
|
|
245
|
+
def move_block_arguments?
|
|
246
|
+
@move_block_arguments
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
##
|
|
250
|
+
# @return [Boolean] Whether to move block results
|
|
251
|
+
#
|
|
252
|
+
def move_block_results?
|
|
253
|
+
@move_block_results
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
##
|
|
257
|
+
# @return [Boolean] Whether to call blocks in-place
|
|
258
|
+
#
|
|
259
|
+
def execute_blocks_in_place?
|
|
260
|
+
@execute_blocks_in_place
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
private
|
|
264
|
+
|
|
265
|
+
def interpret_setting(setting, default)
|
|
266
|
+
if setting.nil?
|
|
267
|
+
default ? true : false
|
|
268
|
+
else
|
|
269
|
+
setting ? true : false
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
101
274
|
##
|
|
102
275
|
# Create a wrapper around the given object.
|
|
103
276
|
#
|
|
104
|
-
# If you pass an optional block, the wrapper itself will be yielded to it
|
|
105
|
-
# at which time you can set additional configuration options.
|
|
106
|
-
# configuration
|
|
277
|
+
# If you pass an optional block, the wrapper itself will be yielded to it,
|
|
278
|
+
# at which time you can set additional configuration options. In
|
|
279
|
+
# particular, method-specific configuration must be set in this block.
|
|
280
|
+
# The configuration is frozen once the object is constructed.
|
|
107
281
|
#
|
|
108
282
|
# @param object [Object] The non-shareable object to wrap.
|
|
109
|
-
# @param
|
|
110
|
-
#
|
|
283
|
+
# @param use_current_ractor [boolean] If true, the wrapper is run in a
|
|
284
|
+
# thread in the current Ractor instead of spawning a new Ractor (the
|
|
285
|
+
# default behavior). This option can be used if the wrapped object
|
|
286
|
+
# cannot be moved or must run in the main Ractor.
|
|
287
|
+
# @param name [String] A name for this wrapper. Used during logging.
|
|
288
|
+
# @param threads [Integer] The number of worker threads to run.
|
|
289
|
+
# Defaults to 0, which causes the wrapper to run sequentially without
|
|
290
|
+
# spawning workers.
|
|
291
|
+
# @param move_data [boolean] If true, all communication will by default
|
|
292
|
+
# move instead of copy arguments and return values. Default is false.
|
|
293
|
+
# This setting can be overridden by other `:move_*` settings.
|
|
294
|
+
# @param move_arguments [boolean] If true, all arguments will be moved
|
|
295
|
+
# instead of copied by default. If not set, uses the `:move_data`
|
|
296
|
+
# setting.
|
|
297
|
+
# @param move_results [boolean] If true, return values are moved instead of
|
|
298
|
+
# copied by default. If not set, uses the `:move_data` setting.
|
|
299
|
+
# @param move_block_arguments [boolean] If true, arguments to blocks are
|
|
300
|
+
# moved instead of copied by default. If not set, uses the `:move_data`
|
|
301
|
+
# setting.
|
|
302
|
+
# @param move_block_results [boolean] If true, result values from blocks
|
|
303
|
+
# are moved instead of copied by default. If not set, uses the
|
|
304
|
+
# `:move_data` setting.
|
|
305
|
+
# @param execute_blocks_in_place [boolean] If true, blocks passed to
|
|
306
|
+
# methods are made shareable and passed into the wrapper to be executed
|
|
307
|
+
# in the wrapped environment. If false (the default), blocks are
|
|
308
|
+
# replaced by a proc that passes messages back out to the caller and
|
|
309
|
+
# executes the block in the caller's environment.
|
|
310
|
+
# @param enable_logging [boolean] Set to true to enable logging. Default
|
|
311
|
+
# is false.
|
|
111
312
|
#
|
|
112
|
-
def initialize(object,
|
|
313
|
+
def initialize(object,
|
|
314
|
+
use_current_ractor: false,
|
|
315
|
+
name: nil,
|
|
316
|
+
threads: 0,
|
|
317
|
+
move_data: false,
|
|
318
|
+
move_arguments: nil,
|
|
319
|
+
move_results: nil,
|
|
320
|
+
move_block_arguments: nil,
|
|
321
|
+
move_block_results: nil,
|
|
322
|
+
execute_blocks_in_place: nil,
|
|
323
|
+
enable_logging: false)
|
|
324
|
+
raise ::Ractor::MovedError, "cannot wrap a moved object" if ::Ractor::MovedObject === object
|
|
325
|
+
|
|
326
|
+
@method_settings = {}
|
|
327
|
+
self.name = name || object_id.to_s
|
|
328
|
+
self.enable_logging = enable_logging
|
|
113
329
|
self.threads = threads
|
|
114
|
-
|
|
115
|
-
|
|
330
|
+
configure_method(move_data: move_data,
|
|
331
|
+
move_arguments: move_arguments,
|
|
332
|
+
move_results: move_results,
|
|
333
|
+
move_block_arguments: move_block_arguments,
|
|
334
|
+
move_block_results: move_block_results,
|
|
335
|
+
execute_blocks_in_place: execute_blocks_in_place)
|
|
116
336
|
yield self if block_given?
|
|
337
|
+
@method_settings.freeze
|
|
117
338
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
maybe_log("Server ready")
|
|
339
|
+
if use_current_ractor
|
|
340
|
+
setup_local_server(object)
|
|
341
|
+
else
|
|
342
|
+
setup_isolated_server(object)
|
|
343
|
+
end
|
|
124
344
|
@stub = Stub.new(self)
|
|
345
|
+
|
|
125
346
|
freeze
|
|
126
347
|
end
|
|
127
348
|
|
|
128
349
|
##
|
|
129
350
|
# Set the number of threads to run in the wrapper. If the underlying object
|
|
130
|
-
# is thread-safe,
|
|
131
|
-
# object is not thread-safe, you should leave this
|
|
132
|
-
#
|
|
133
|
-
#
|
|
351
|
+
# is thread-safe, setting a value of 2 or more allows concurrent calls to
|
|
352
|
+
# it. If the underlying object is not thread-safe, you should leave this
|
|
353
|
+
# set to its default of 0, which disables worker threads and handles all
|
|
354
|
+
# calls sequentially.
|
|
134
355
|
#
|
|
135
356
|
# This method can be called only during an initialization block.
|
|
357
|
+
# All settings are frozen once the wrapper is active.
|
|
136
358
|
#
|
|
137
|
-
# @param value [Integer
|
|
359
|
+
# @param value [Integer]
|
|
138
360
|
#
|
|
139
361
|
def threads=(value)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@threads = value
|
|
144
|
-
else
|
|
145
|
-
@threads = nil
|
|
146
|
-
end
|
|
362
|
+
value = value.to_i
|
|
363
|
+
value = 0 if value.negative?
|
|
364
|
+
@threads = value
|
|
147
365
|
end
|
|
148
366
|
|
|
149
367
|
##
|
|
150
368
|
# Enable or disable internal debug logging.
|
|
151
369
|
#
|
|
152
370
|
# This method can be called only during an initialization block.
|
|
371
|
+
# All settings are frozen once the wrapper is active.
|
|
153
372
|
#
|
|
154
373
|
# @param value [Boolean]
|
|
155
374
|
#
|
|
156
|
-
def
|
|
157
|
-
@
|
|
375
|
+
def enable_logging=(value)
|
|
376
|
+
@enable_logging = value ? true : false
|
|
158
377
|
end
|
|
159
378
|
|
|
160
379
|
##
|
|
161
|
-
# Set the name of this wrapper
|
|
380
|
+
# Set the name of this wrapper. This is shown in logging, and is also used
|
|
381
|
+
# as the name of the wrapping Ractor.
|
|
162
382
|
#
|
|
163
383
|
# This method can be called only during an initialization block.
|
|
384
|
+
# All settings are frozen once the wrapper is active.
|
|
164
385
|
#
|
|
165
386
|
# @param value [String, nil]
|
|
166
387
|
#
|
|
@@ -169,56 +390,142 @@ class Ractor
|
|
|
169
390
|
end
|
|
170
391
|
|
|
171
392
|
##
|
|
172
|
-
#
|
|
173
|
-
#
|
|
393
|
+
# Configure the move semantics for the given method (or the default
|
|
394
|
+
# settings if no method name is given.) That is, determine whether
|
|
395
|
+
# arguments, return values, and/or exceptions are copied or moved when
|
|
396
|
+
# communicated with the wrapper. By default, all objects are copied.
|
|
174
397
|
#
|
|
175
|
-
#
|
|
398
|
+
# This method can be called only during an initialization block.
|
|
399
|
+
# All settings are frozen once the wrapper is active.
|
|
176
400
|
#
|
|
177
|
-
|
|
401
|
+
# @param method_name [Symbol, nil] The name of the method being configured,
|
|
402
|
+
# or `nil` to set defaults for all methods not configured explicitly.
|
|
403
|
+
# @param move_data [boolean] If true, communication for this method will
|
|
404
|
+
# move instead of copy arguments and return values. Default is false.
|
|
405
|
+
# This setting can be overridden by other `:move_*` settings.
|
|
406
|
+
# @param move_arguments [boolean] If true, arguments for this method are
|
|
407
|
+
# moved instead of copied. If not set, uses the `:move_data` setting.
|
|
408
|
+
# @param move_results [boolean] If true, return values for this method are
|
|
409
|
+
# moved instead of copied. If not set, uses the `:move_data` setting.
|
|
410
|
+
# @param move_block_arguments [boolean] If true, arguments to blocks passed
|
|
411
|
+
# to this method are moved instead of copied. If not set, uses the
|
|
412
|
+
# `:move_data` setting.
|
|
413
|
+
# @param move_block_results [boolean] If true, result values from blocks
|
|
414
|
+
# passed to this method are moved instead of copied. If not set, uses
|
|
415
|
+
# the `:move_data` setting.
|
|
416
|
+
# @param execute_blocks_in_place [boolean] If true, blocks passed to this
|
|
417
|
+
# method are made shareable and passed into the wrapper to be executed
|
|
418
|
+
# in the wrapped environment. If false (the default), blocks are
|
|
419
|
+
# replaced by a proc that passes messages back out to the caller and
|
|
420
|
+
# executes the block in the caller's environment.
|
|
421
|
+
#
|
|
422
|
+
def configure_method(method_name = nil,
|
|
423
|
+
move_data: false,
|
|
424
|
+
move_arguments: nil,
|
|
425
|
+
move_results: nil,
|
|
426
|
+
move_block_arguments: nil,
|
|
427
|
+
move_block_results: nil,
|
|
428
|
+
execute_blocks_in_place: nil)
|
|
429
|
+
method_name = method_name.to_sym unless method_name.nil?
|
|
430
|
+
@method_settings[method_name] =
|
|
431
|
+
MethodSettings.new(move_data: move_data,
|
|
432
|
+
move_arguments: move_arguments,
|
|
433
|
+
move_results: move_results,
|
|
434
|
+
move_block_arguments: move_block_arguments,
|
|
435
|
+
move_block_results: move_block_results,
|
|
436
|
+
execute_blocks_in_place: execute_blocks_in_place)
|
|
437
|
+
end
|
|
178
438
|
|
|
179
439
|
##
|
|
180
|
-
# Return the
|
|
181
|
-
# no threading.
|
|
440
|
+
# Return the name of this wrapper.
|
|
182
441
|
#
|
|
183
|
-
# @return [
|
|
442
|
+
# @return [String]
|
|
184
443
|
#
|
|
185
|
-
attr_reader :
|
|
444
|
+
attr_reader :name
|
|
186
445
|
|
|
187
446
|
##
|
|
188
|
-
#
|
|
447
|
+
# Determine whether this wrapper runs in the current Ractor
|
|
448
|
+
#
|
|
449
|
+
# @return [boolean]
|
|
450
|
+
#
|
|
451
|
+
def use_current_ractor?
|
|
452
|
+
@ractor.nil?
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
##
|
|
456
|
+
# Return whether logging is enabled for this wrapper.
|
|
189
457
|
#
|
|
190
458
|
# @return [Boolean]
|
|
191
459
|
#
|
|
192
|
-
|
|
460
|
+
def enable_logging?
|
|
461
|
+
@enable_logging
|
|
462
|
+
end
|
|
193
463
|
|
|
194
464
|
##
|
|
195
|
-
# Return the
|
|
465
|
+
# Return the number of worker threads used by the wrapper.
|
|
196
466
|
#
|
|
197
|
-
# @return [
|
|
467
|
+
# @return [Integer]
|
|
198
468
|
#
|
|
199
|
-
attr_reader :
|
|
469
|
+
attr_reader :threads
|
|
470
|
+
|
|
471
|
+
##
|
|
472
|
+
# Return the method settings for the given method name. This returns the
|
|
473
|
+
# default method settings if the given method is not configured explicitly
|
|
474
|
+
# by name.
|
|
475
|
+
#
|
|
476
|
+
# @param method_name [Symbol,nil] The method name, or `nil` to return the
|
|
477
|
+
# defaults.
|
|
478
|
+
# @return [MethodSettings]
|
|
479
|
+
#
|
|
480
|
+
def method_settings(method_name)
|
|
481
|
+
method_name = method_name.to_sym
|
|
482
|
+
@method_settings[method_name] || @method_settings[nil]
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
##
|
|
486
|
+
# Return the wrapper stub. This is an object that responds to the same
|
|
487
|
+
# methods as the wrapped object, providing an easy way to call a wrapper.
|
|
488
|
+
#
|
|
489
|
+
# @return [Ractor::Wrapper::Stub]
|
|
490
|
+
#
|
|
491
|
+
attr_reader :stub
|
|
200
492
|
|
|
201
493
|
##
|
|
202
|
-
# A lower-level interface for calling the wrapper.
|
|
494
|
+
# A lower-level interface for calling methods through the wrapper.
|
|
203
495
|
#
|
|
204
496
|
# @param method_name [Symbol] The name of the method to call
|
|
205
497
|
# @param args [arguments] The positional arguments
|
|
206
498
|
# @param kwargs [keywords] The keyword arguments
|
|
207
499
|
# @return [Object] The return value
|
|
208
500
|
#
|
|
209
|
-
def call(method_name, *args, **kwargs)
|
|
210
|
-
|
|
211
|
-
transaction =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
501
|
+
def call(method_name, *args, **kwargs, &)
|
|
502
|
+
reply_port = ::Ractor::Port.new
|
|
503
|
+
transaction = ::Random.rand(7_958_661_109_946_400_884_391_936).to_s(36).freeze
|
|
504
|
+
settings = method_settings(method_name)
|
|
505
|
+
block_arg = make_block_arg(settings, &)
|
|
506
|
+
message = CallMessage.new(method_name: method_name,
|
|
507
|
+
args: args,
|
|
508
|
+
kwargs: kwargs,
|
|
509
|
+
block_arg: block_arg,
|
|
510
|
+
transaction: transaction,
|
|
511
|
+
settings: settings,
|
|
512
|
+
reply_port: reply_port)
|
|
513
|
+
maybe_log("Sending method", method_name: method_name, transaction: transaction)
|
|
514
|
+
@port.send(message, move: settings.move_arguments?)
|
|
515
|
+
loop do
|
|
516
|
+
reply_message = reply_port.receive
|
|
517
|
+
case reply_message
|
|
518
|
+
when YieldMessage
|
|
519
|
+
handle_yield(reply_message, transaction, settings, method_name, &)
|
|
520
|
+
when ReturnMessage
|
|
521
|
+
maybe_log("Received result", method_name: method_name, transaction: transaction)
|
|
522
|
+
reply_port.close
|
|
523
|
+
return reply_message.value
|
|
524
|
+
when ExceptionMessage
|
|
525
|
+
maybe_log("Received exception", method_name: method_name, transaction: transaction)
|
|
526
|
+
reply_port.close
|
|
527
|
+
raise reply_message.exception
|
|
528
|
+
end
|
|
222
529
|
end
|
|
223
530
|
end
|
|
224
531
|
|
|
@@ -226,14 +533,14 @@ class Ractor
|
|
|
226
533
|
# Request that the wrapper stop. All currently running calls will complete
|
|
227
534
|
# before the wrapper actually terminates. However, any new calls will fail.
|
|
228
535
|
#
|
|
229
|
-
# This
|
|
536
|
+
# This method is idempotent and can be called multiple times (even from
|
|
230
537
|
# different ractors).
|
|
231
538
|
#
|
|
232
539
|
# @return [self]
|
|
233
540
|
#
|
|
234
541
|
def async_stop
|
|
235
|
-
maybe_log("Stopping
|
|
236
|
-
@
|
|
542
|
+
maybe_log("Stopping wrapper")
|
|
543
|
+
@port.send(StopMessage.new.freeze)
|
|
237
544
|
self
|
|
238
545
|
rescue ::Ractor::ClosedError
|
|
239
546
|
# Ignore to allow stops to be idempotent.
|
|
@@ -241,87 +548,241 @@ class Ractor
|
|
|
241
548
|
end
|
|
242
549
|
|
|
243
550
|
##
|
|
244
|
-
#
|
|
245
|
-
#
|
|
246
|
-
#
|
|
551
|
+
# Blocks until the wrapper has fully stopped.
|
|
552
|
+
#
|
|
553
|
+
# @return [self]
|
|
554
|
+
#
|
|
555
|
+
def join
|
|
556
|
+
if @ractor
|
|
557
|
+
@ractor.join
|
|
558
|
+
else
|
|
559
|
+
reply_port = ::Ractor::Port.new
|
|
560
|
+
@port.send(JoinMessage.new(reply_port))
|
|
561
|
+
reply_port.receive
|
|
562
|
+
reply_port.close
|
|
563
|
+
end
|
|
564
|
+
self
|
|
565
|
+
rescue ::Ractor::ClosedError
|
|
566
|
+
self
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
##
|
|
570
|
+
# Retrieves the original object that was wrapped. This should be called
|
|
571
|
+
# only after a stop request has been issued using {#async_stop}, and may
|
|
572
|
+
# block until the wrapper has fully stopped.
|
|
573
|
+
#
|
|
574
|
+
# This can be called only if the wrapper was *not* configured with
|
|
575
|
+
# `use_current_ractor: true`. If the wrapper had that configuration, the
|
|
576
|
+
# object will not be moved, and does not need to be recovered. In such a
|
|
577
|
+
# case, any calls to this method will raise Ractor::Error.
|
|
578
|
+
#
|
|
579
|
+
# Only one ractor may call this method; any additional calls will fail with
|
|
580
|
+
# a Ractor::Error.
|
|
247
581
|
#
|
|
248
582
|
# @return [Object] The original wrapped object
|
|
249
583
|
#
|
|
250
|
-
def
|
|
251
|
-
@ractor
|
|
584
|
+
def recover_object
|
|
585
|
+
raise ::Ractor::Error, "cannot recover an object from a local wrapper" unless @ractor
|
|
586
|
+
@ractor.value
|
|
252
587
|
end
|
|
253
588
|
|
|
254
|
-
private
|
|
589
|
+
#### private items below ####
|
|
255
590
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
end
|
|
591
|
+
##
|
|
592
|
+
# @private
|
|
593
|
+
# Message sent to initialize a server.
|
|
594
|
+
#
|
|
595
|
+
InitMessage = ::Data.define(:object, :enable_logging, :threads)
|
|
262
596
|
|
|
263
597
|
##
|
|
264
|
-
#
|
|
598
|
+
# @private
|
|
599
|
+
# Message sent to a server to call a method
|
|
265
600
|
#
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
# Create a stub given a wrapper.
|
|
269
|
-
#
|
|
270
|
-
# @param wrapper [Ractor::Wrapper]
|
|
271
|
-
#
|
|
272
|
-
def initialize(wrapper)
|
|
273
|
-
@wrapper = wrapper
|
|
274
|
-
freeze
|
|
275
|
-
end
|
|
601
|
+
CallMessage = ::Data.define(:method_name, :args, :kwargs, :block_arg,
|
|
602
|
+
:transaction, :settings, :reply_port)
|
|
276
603
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
end
|
|
604
|
+
##
|
|
605
|
+
# @private
|
|
606
|
+
# Message sent to a server when a worker thread terminates
|
|
607
|
+
#
|
|
608
|
+
WorkerStoppedMessage = ::Data.define(:worker_num)
|
|
283
609
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
610
|
+
##
|
|
611
|
+
# @private
|
|
612
|
+
# Message sent to a server to request it to stop
|
|
613
|
+
#
|
|
614
|
+
StopMessage = ::Data.define
|
|
615
|
+
|
|
616
|
+
##
|
|
617
|
+
# @private
|
|
618
|
+
# Message sent to a server to request a join response
|
|
619
|
+
#
|
|
620
|
+
JoinMessage = ::Data.define(:reply_port)
|
|
621
|
+
|
|
622
|
+
##
|
|
623
|
+
# @private
|
|
624
|
+
# Message sent to report a return value
|
|
625
|
+
#
|
|
626
|
+
ReturnMessage = ::Data.define(:value)
|
|
627
|
+
|
|
628
|
+
##
|
|
629
|
+
# @private
|
|
630
|
+
# Message sent to report an exception result
|
|
631
|
+
#
|
|
632
|
+
ExceptionMessage = ::Data.define(:exception)
|
|
633
|
+
|
|
634
|
+
##
|
|
635
|
+
# @private
|
|
636
|
+
# Message sent from a server to request a yield block run
|
|
637
|
+
#
|
|
638
|
+
YieldMessage = ::Data.define(:args, :kwargs, :reply_port)
|
|
639
|
+
|
|
640
|
+
private
|
|
641
|
+
|
|
642
|
+
##
|
|
643
|
+
# Start a server in the current Ractor.
|
|
644
|
+
# Passes the object directly to the server.
|
|
645
|
+
#
|
|
646
|
+
def setup_local_server(object)
|
|
647
|
+
maybe_log("Starting local server")
|
|
648
|
+
@ractor = nil
|
|
649
|
+
@port = ::Ractor::Port.new
|
|
650
|
+
::Thread.new do
|
|
651
|
+
Server.run_local(object: object,
|
|
652
|
+
port: @port,
|
|
653
|
+
name: name,
|
|
654
|
+
enable_logging: enable_logging?,
|
|
655
|
+
threads: threads)
|
|
287
656
|
end
|
|
288
657
|
end
|
|
289
658
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
659
|
+
##
|
|
660
|
+
# Start a server in an isolated Ractor.
|
|
661
|
+
# This must send the object separately since it must be moved into the
|
|
662
|
+
# server's Ractor.
|
|
663
|
+
#
|
|
664
|
+
def setup_isolated_server(object)
|
|
665
|
+
maybe_log("Starting isolated server")
|
|
666
|
+
@ractor = ::Ractor.new(name, enable_logging?, threads, name: "wrapper:#{name}") do |name, enable_logging, threads|
|
|
667
|
+
Server.run_isolated(name: name,
|
|
668
|
+
enable_logging: enable_logging,
|
|
669
|
+
threads: threads)
|
|
298
670
|
end
|
|
671
|
+
@port = @ractor.default_port
|
|
672
|
+
@port.send(object, move: true)
|
|
673
|
+
end
|
|
299
674
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
675
|
+
##
|
|
676
|
+
# Create a transaction ID, used for logging
|
|
677
|
+
#
|
|
678
|
+
def make_transaction
|
|
679
|
+
::Random.rand(7_958_661_109_946_400_884_391_936).to_s(36).freeze
|
|
680
|
+
end
|
|
304
681
|
|
|
305
|
-
|
|
682
|
+
##
|
|
683
|
+
# Create the shareable object representing a block in a method call
|
|
684
|
+
#
|
|
685
|
+
def make_block_arg(settings, &)
|
|
686
|
+
if !block_given?
|
|
687
|
+
nil
|
|
688
|
+
elsif settings.execute_blocks_in_place?
|
|
689
|
+
::Ractor.shareable_proc(&)
|
|
690
|
+
else
|
|
691
|
+
:send_block_message
|
|
692
|
+
end
|
|
693
|
+
end
|
|
306
694
|
|
|
307
|
-
|
|
308
|
-
|
|
695
|
+
##
|
|
696
|
+
# Handle a call to a block directed to run in the caller environment.
|
|
697
|
+
#
|
|
698
|
+
def handle_yield(message, transaction, settings, method_name)
|
|
699
|
+
maybe_log("Yielding to block", method_name: method_name, transaction: transaction)
|
|
700
|
+
begin
|
|
701
|
+
block_result = yield(*message.args, **message.kwargs)
|
|
702
|
+
maybe_log("Sending block result", method_name: method_name, transaction: transaction)
|
|
703
|
+
message.reply_port.send(ReturnMessage.new(block_result), move: settings.move_block_results?)
|
|
704
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
705
|
+
maybe_log("Sending block exception", method_name: method_name, transaction: transaction)
|
|
706
|
+
begin
|
|
707
|
+
message.reply_port.send(ExceptionMessage.new(e))
|
|
708
|
+
rescue ::StandardError
|
|
709
|
+
begin
|
|
710
|
+
message.reply_port.send(ExceptionMessage.new(::StandardError.new(e.inspect)))
|
|
711
|
+
rescue ::StandardError
|
|
712
|
+
maybe_log("Failure to send block reply", method_name: method_name, transaction: transaction)
|
|
713
|
+
end
|
|
714
|
+
end
|
|
309
715
|
end
|
|
310
716
|
end
|
|
311
717
|
|
|
718
|
+
##
|
|
719
|
+
# Prints out a log message
|
|
720
|
+
#
|
|
721
|
+
def maybe_log(str, transaction: nil, method_name: nil)
|
|
722
|
+
return unless enable_logging?
|
|
723
|
+
metadata = [::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%L"), "Ractor::Wrapper/#{name}"]
|
|
724
|
+
metadata << "Transaction/#{transaction}" if transaction
|
|
725
|
+
metadata << "Method/#{method_name}" if method_name
|
|
726
|
+
metadata = metadata.join(" ")
|
|
727
|
+
$stderr.puts("[#{metadata}] #{str}")
|
|
728
|
+
$stderr.flush
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
##
|
|
312
732
|
# @private
|
|
733
|
+
#
|
|
734
|
+
# Server is the backend implementation of a wrapper. It listens for method
|
|
735
|
+
# call requests on a port, and calls the wrapped object in a controlled
|
|
736
|
+
# environment.
|
|
737
|
+
#
|
|
738
|
+
# It can run:
|
|
739
|
+
#
|
|
740
|
+
# * Either hosted by an external Ractor or isolated in a dedicated Ractor
|
|
741
|
+
# * Either sequentially or concurrently using worker threads.
|
|
742
|
+
#
|
|
313
743
|
class Server
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
744
|
+
##
|
|
745
|
+
# @private
|
|
746
|
+
# Create and run a server hosted in the current Ractor
|
|
747
|
+
#
|
|
748
|
+
def self.run_local(object:, port:, name:, enable_logging: false, threads: 0)
|
|
749
|
+
server = new(isolated: false, object:, port:, name:, enable_logging:, threads:)
|
|
750
|
+
server.run
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
##
|
|
754
|
+
# @private
|
|
755
|
+
# Create and run a server in an isolated Ractor
|
|
756
|
+
#
|
|
757
|
+
def self.run_isolated(name:, enable_logging: false, threads: 0)
|
|
758
|
+
port = ::Ractor.current.default_port
|
|
759
|
+
server = new(isolated: true, object: nil, port:, name:, enable_logging:, threads:)
|
|
760
|
+
server.run
|
|
761
|
+
end
|
|
319
762
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
763
|
+
# @private
|
|
764
|
+
def initialize(isolated:, object:, port:, name:, enable_logging:, threads:)
|
|
765
|
+
@isolated = isolated
|
|
766
|
+
@object = object
|
|
767
|
+
@port = port
|
|
768
|
+
@name = name
|
|
769
|
+
@enable_logging = enable_logging
|
|
770
|
+
@threads = threads.positive? ? threads : nil
|
|
771
|
+
@join_requests = []
|
|
772
|
+
end
|
|
324
773
|
|
|
774
|
+
##
|
|
775
|
+
# @private
|
|
776
|
+
# Handle the server lifecycle.
|
|
777
|
+
# Returns the wrapped object, so it can be recovered if the server is run
|
|
778
|
+
# in a Ractor.
|
|
779
|
+
#
|
|
780
|
+
def run
|
|
781
|
+
receive_remote_object if @isolated
|
|
782
|
+
start_workers if @threads
|
|
783
|
+
main_loop
|
|
784
|
+
stop_workers if @threads
|
|
785
|
+
cleanup
|
|
325
786
|
@object
|
|
326
787
|
rescue ::StandardError => e
|
|
327
788
|
maybe_log("Unexpected error: #{e.inspect}")
|
|
@@ -330,112 +791,260 @@ class Ractor
|
|
|
330
791
|
|
|
331
792
|
private
|
|
332
793
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
::Thread.new { monitor_thread(threads) }
|
|
341
|
-
queue
|
|
794
|
+
##
|
|
795
|
+
# Receive the moved remote object. Called if the server is run in a
|
|
796
|
+
# separate Ractor.
|
|
797
|
+
#
|
|
798
|
+
def receive_remote_object
|
|
799
|
+
maybe_log("Waiting for remote object")
|
|
800
|
+
@object = @port.receive
|
|
342
801
|
end
|
|
343
802
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
803
|
+
##
|
|
804
|
+
# Start the worker threads. Each thread picks up methods to run from a
|
|
805
|
+
# shared queue. Called only if worker threading is enabled.
|
|
806
|
+
#
|
|
807
|
+
def start_workers
|
|
808
|
+
@queue = ::Queue.new
|
|
809
|
+
maybe_log("Spawning #{@threads} worker threads")
|
|
810
|
+
(1..@threads).map do |worker_num|
|
|
811
|
+
::Thread.new { worker_thread(worker_num) }
|
|
353
812
|
end
|
|
354
|
-
maybe_worker_log(worker_num, "Stopping")
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
def monitor_thread(workers)
|
|
358
|
-
workers.each(&:join)
|
|
359
|
-
maybe_log("All workers finished")
|
|
360
|
-
::Ractor.current.send(Message.new(:threads_stopped))
|
|
361
813
|
end
|
|
362
814
|
|
|
363
|
-
|
|
815
|
+
##
|
|
816
|
+
# This is the main loop, listening on the inbox and handling messages for
|
|
817
|
+
# normal operation:
|
|
818
|
+
#
|
|
819
|
+
# * If it receives a CallMessage, it either runs the method (when in
|
|
820
|
+
# sequential mode) or adds it to the job queue (when in worker mode).
|
|
821
|
+
# * If it receives a StopMessage, it exits the main loop and proceeds
|
|
822
|
+
# to the termination logic.
|
|
823
|
+
# * If it receives a JoinMessage, it adds it to the list of join ports
|
|
824
|
+
# to notify once the wrapper completes.
|
|
825
|
+
# * If it receives a WorkerStoppedMessage, that indicates a worker
|
|
826
|
+
# thread has unexpectedly stopped. We conclude something has gone
|
|
827
|
+
# wrong with a worker, and we bail, stopping the remaining workers
|
|
828
|
+
# and proceeding to termination logic.
|
|
829
|
+
#
|
|
830
|
+
def main_loop
|
|
364
831
|
loop do
|
|
365
|
-
maybe_log("Waiting for message")
|
|
366
|
-
|
|
367
|
-
case
|
|
368
|
-
when
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
832
|
+
maybe_log("Waiting for message in running phase")
|
|
833
|
+
message = @port.receive
|
|
834
|
+
case message
|
|
835
|
+
when CallMessage
|
|
836
|
+
maybe_log("Received CallMessage", call_message: message)
|
|
837
|
+
if @threads
|
|
838
|
+
@queue.enq(message)
|
|
372
839
|
else
|
|
373
|
-
handle_method(
|
|
840
|
+
handle_method(message)
|
|
374
841
|
end
|
|
375
|
-
when
|
|
842
|
+
when WorkerStoppedMessage
|
|
843
|
+
maybe_log("Received unexpected WorkerStoppedMessage")
|
|
844
|
+
@threads -= 1 if @threads
|
|
845
|
+
break
|
|
846
|
+
when StopMessage
|
|
376
847
|
maybe_log("Received stop")
|
|
377
|
-
queue&.close
|
|
378
848
|
break
|
|
849
|
+
when JoinMessage
|
|
850
|
+
maybe_log("Received and queueing join request")
|
|
851
|
+
@join_requests << message.reply_port
|
|
379
852
|
end
|
|
380
853
|
end
|
|
381
854
|
end
|
|
382
855
|
|
|
383
|
-
|
|
856
|
+
##
|
|
857
|
+
# This signals workers to stop by closing the queue, and then waits for
|
|
858
|
+
# all workers to report in that they have stopped. It is called only if
|
|
859
|
+
# worker threading is enabled.
|
|
860
|
+
#
|
|
861
|
+
# Responds to messages to indicate the wrapper is stopping and no longer
|
|
862
|
+
# accepting new method requests:
|
|
863
|
+
#
|
|
864
|
+
# * If it receives a CallMessage, it sends back a refusal exception.
|
|
865
|
+
# * If it receives a StopMessage, it does nothing (i.e. the stop
|
|
866
|
+
# operation is idempotent).
|
|
867
|
+
# * If it receives a JoinMessage, it adds it to the list of join ports
|
|
868
|
+
# to notify once the wrapper completes. At this point the wrapper is
|
|
869
|
+
# not yet considered complete because workers are still processing
|
|
870
|
+
# earlier method calls.
|
|
871
|
+
# * If it receives a WorkerStoppedMessage, it updates its count of
|
|
872
|
+
# running workers.
|
|
873
|
+
#
|
|
874
|
+
# This phase continues until all workers have signaled that they have
|
|
875
|
+
# stopped.
|
|
876
|
+
#
|
|
877
|
+
def stop_workers
|
|
878
|
+
@queue.close
|
|
879
|
+
while @threads.positive?
|
|
880
|
+
maybe_log("Waiting for message in stopping phase")
|
|
881
|
+
message = @port.receive
|
|
882
|
+
case message
|
|
883
|
+
when CallMessage
|
|
884
|
+
refuse_method(message)
|
|
885
|
+
when WorkerStoppedMessage
|
|
886
|
+
maybe_log("Acknowledged WorkerStoppedMessage: #{message.worker_num}")
|
|
887
|
+
@threads -= 1
|
|
888
|
+
when StopMessage
|
|
889
|
+
maybe_log("Stop received when already stopping")
|
|
890
|
+
when JoinMessage
|
|
891
|
+
maybe_log("Received and queueing join request")
|
|
892
|
+
@join_requests << message.reply_port
|
|
893
|
+
end
|
|
894
|
+
end
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
##
|
|
898
|
+
# This is called when the Server is ready to terminate completely.
|
|
899
|
+
# It closes the inbox and responds to any remaining contents.
|
|
900
|
+
#
|
|
901
|
+
def cleanup
|
|
902
|
+
maybe_log("Closing inbox")
|
|
903
|
+
@port.close
|
|
904
|
+
maybe_log("Responding to join requests")
|
|
905
|
+
@join_requests.each { |port| send_join_reply(port) }
|
|
906
|
+
maybe_log("Draining inbox")
|
|
384
907
|
loop do
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
908
|
+
message = begin
|
|
909
|
+
@port.receive
|
|
910
|
+
rescue ::Ractor::ClosedError
|
|
911
|
+
maybe_log("Inbox is empty")
|
|
912
|
+
nil
|
|
913
|
+
end
|
|
914
|
+
break if message.nil?
|
|
915
|
+
case message
|
|
916
|
+
when CallMessage
|
|
389
917
|
refuse_method(message)
|
|
390
|
-
when
|
|
391
|
-
|
|
918
|
+
when WorkerStoppedMessage
|
|
919
|
+
maybe_log("Unexpected WorkerStoppedMessage when in cleanup")
|
|
920
|
+
when StopMessage
|
|
921
|
+
maybe_log("Stop received when already stopping")
|
|
922
|
+
when JoinMessage
|
|
923
|
+
maybe_log("Received and responding immediately to join request")
|
|
924
|
+
send_join_reply(message.reply_port)
|
|
392
925
|
end
|
|
393
926
|
end
|
|
394
927
|
end
|
|
395
928
|
|
|
396
|
-
|
|
397
|
-
|
|
929
|
+
##
|
|
930
|
+
# A worker thread repeatedly pulls a method call requests off the job
|
|
931
|
+
# queue, handles it, and sends back a response. It also removes the
|
|
932
|
+
# request from the pending request list to signal that it has responded.
|
|
933
|
+
# If no job is available, the thread blocks while waiting. If the queue
|
|
934
|
+
# is closed, the worker will send an acknowledgement message and then
|
|
935
|
+
# terminate.
|
|
936
|
+
#
|
|
937
|
+
def worker_thread(worker_num)
|
|
938
|
+
maybe_log("Worker starting", worker_num: worker_num)
|
|
398
939
|
loop do
|
|
399
|
-
maybe_log("
|
|
400
|
-
message =
|
|
401
|
-
|
|
940
|
+
maybe_log("Waiting for job", worker_num: worker_num)
|
|
941
|
+
message = @queue.deq
|
|
942
|
+
break if message.nil?
|
|
943
|
+
handle_method(message, worker_num: worker_num)
|
|
944
|
+
end
|
|
945
|
+
ensure
|
|
946
|
+
maybe_log("Worker stopping", worker_num: worker_num)
|
|
947
|
+
begin
|
|
948
|
+
@port.send(WorkerStoppedMessage.new(worker_num))
|
|
949
|
+
rescue ::Ractor::ClosedError
|
|
950
|
+
maybe_log("Orphaned worker thread", worker_num: worker_num)
|
|
402
951
|
end
|
|
403
|
-
rescue ::Ractor::ClosedError
|
|
404
|
-
maybe_log("Queue is empty")
|
|
405
952
|
end
|
|
406
953
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
954
|
+
##
|
|
955
|
+
# This is called to handle a method call request.
|
|
956
|
+
# It calls the method on the wrapped object, and then sends back a
|
|
957
|
+
# response to the caller. If an exception was raised, it sends back an
|
|
958
|
+
# error response. It tries very hard always to send a response of some
|
|
959
|
+
# kind; if an error occurs while constructing or sending a response, it
|
|
960
|
+
# will catch the exception and try to send a simpler response. If a block
|
|
961
|
+
# was passed to the method, it is also handled here.
|
|
962
|
+
#
|
|
963
|
+
def handle_method(message, worker_num: nil)
|
|
964
|
+
block = make_block(message)
|
|
965
|
+
maybe_log("Running method", worker_num: worker_num, call_message: message)
|
|
412
966
|
begin
|
|
413
|
-
result = @object.
|
|
414
|
-
|
|
415
|
-
|
|
967
|
+
result = @object.__send__(message.method_name, *message.args, **message.kwargs, &block)
|
|
968
|
+
maybe_log("Sending return value", worker_num: worker_num, call_message: message)
|
|
969
|
+
message.reply_port.send(ReturnMessage.new(result), move: message.settings.move_results?)
|
|
416
970
|
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
417
|
-
|
|
418
|
-
|
|
971
|
+
maybe_log("Sending exception", worker_num: worker_num, call_message: message)
|
|
972
|
+
begin
|
|
973
|
+
message.reply_port.send(ExceptionMessage.new(e))
|
|
974
|
+
rescue ::StandardError
|
|
975
|
+
begin
|
|
976
|
+
message.reply_port.send(ExceptionMessage.new(::StandardError.new(e.inspect)))
|
|
977
|
+
rescue ::StandardError
|
|
978
|
+
maybe_log("Failure to send method response", worker_num: worker_num, call_message: message)
|
|
979
|
+
end
|
|
980
|
+
end
|
|
419
981
|
end
|
|
420
982
|
end
|
|
421
983
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
984
|
+
##
|
|
985
|
+
# Creates a block appropriate to the block specification received with
|
|
986
|
+
# the method call message. This could return:
|
|
987
|
+
#
|
|
988
|
+
# * nil if there was no block
|
|
989
|
+
# * the proc itself, if a shareable proc was received
|
|
990
|
+
# * otherwise a proc that sends a message back to the caller, along
|
|
991
|
+
# with the block arguments, to run the block in the caller's
|
|
992
|
+
# environment
|
|
993
|
+
#
|
|
994
|
+
def make_block(message)
|
|
995
|
+
return message.block_arg unless message.block_arg == :send_block_message
|
|
996
|
+
proc do |*args, **kwargs|
|
|
997
|
+
reply_port = ::Ractor::Port.new
|
|
998
|
+
yield_message = YieldMessage.new(args: args, kwargs: kwargs, reply_port: reply_port)
|
|
999
|
+
message.reply_port.send(yield_message, move: message.settings.move_block_arguments?)
|
|
1000
|
+
reply_message = reply_port.receive
|
|
1001
|
+
reply_port.close
|
|
1002
|
+
case reply_message
|
|
1003
|
+
when ExceptionMessage
|
|
1004
|
+
raise reply_message.exception
|
|
1005
|
+
when ReturnMessage
|
|
1006
|
+
reply_message.value
|
|
1007
|
+
end
|
|
1008
|
+
end
|
|
426
1009
|
end
|
|
427
1010
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
1011
|
+
##
|
|
1012
|
+
# This is called from the main Ractor thread to report to a caller that
|
|
1013
|
+
# the wrapper cannot handle a requested method call, likely because the
|
|
1014
|
+
# wrapper is shutting down.
|
|
1015
|
+
#
|
|
1016
|
+
def refuse_method(message)
|
|
1017
|
+
maybe_log("Refusing method call", call_message: message)
|
|
1018
|
+
begin
|
|
1019
|
+
error = ::Ractor::ClosedError.new("Wrapper is shutting down")
|
|
1020
|
+
message.reply_port.send(ExceptionMessage.new(error))
|
|
1021
|
+
rescue ::Ractor::Error
|
|
1022
|
+
maybe_log("Failed to send refusal message", call_message: message)
|
|
1023
|
+
end
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
##
|
|
1027
|
+
# This attempts to send a signal that a wrapper join has completed.
|
|
1028
|
+
#
|
|
1029
|
+
def send_join_reply(port)
|
|
1030
|
+
port.send(nil)
|
|
1031
|
+
rescue ::Ractor::ClosedError
|
|
1032
|
+
maybe_log("Join reply port is closed")
|
|
433
1033
|
end
|
|
434
1034
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
1035
|
+
##
|
|
1036
|
+
# Print out a log message
|
|
1037
|
+
#
|
|
1038
|
+
def maybe_log(str, call_message: nil, worker_num: nil, transaction: nil, method_name: nil)
|
|
1039
|
+
return unless @enable_logging
|
|
1040
|
+
transaction ||= call_message&.transaction
|
|
1041
|
+
method_name ||= call_message&.method_name
|
|
1042
|
+
metadata = [::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%L"), "Ractor::Wrapper/#{@name}"]
|
|
1043
|
+
metadata << "Worker/#{worker_num}" if worker_num
|
|
1044
|
+
metadata << "Transaction/#{transaction}" if transaction
|
|
1045
|
+
metadata << "Method/#{method_name}" if method_name
|
|
1046
|
+
metadata = metadata.join(" ")
|
|
1047
|
+
$stderr.puts("[#{metadata}] #{str}")
|
|
439
1048
|
$stderr.flush
|
|
440
1049
|
end
|
|
441
1050
|
end
|