ractor-wrapper 0.2.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 +14 -0
- data/LICENSE.md +1 -1
- data/README.md +142 -77
- data/lib/ractor/wrapper/version.rb +3 -1
- data/lib/ractor/wrapper.rb +763 -383
- data/lib/ractor-wrapper.rb +2 -0
- metadata +8 -12
data/lib/ractor/wrapper.rb
CHANGED
|
@@ -1,156 +1,357 @@
|
|
|
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.
|
|
8
10
|
#
|
|
9
11
|
# WARNING: This is a highly experimental library, and currently _not_
|
|
10
|
-
# recommended for production use. (As of Ruby
|
|
12
|
+
# recommended for production use. (As of Ruby 4.0.0, the same can be said of
|
|
11
13
|
# Ractors in general.)
|
|
12
14
|
#
|
|
13
15
|
# ## What is Ractor::Wrapper?
|
|
14
16
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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
|
|
20
86
|
#
|
|
21
|
-
#
|
|
22
|
-
# implemented as an object and accessed using ordinary method calls. It does
|
|
23
|
-
# this by "wrapping" the object in a Ractor, and mapping method calls to
|
|
24
|
-
# message passing. This may make it easier to implement such a resource with
|
|
25
|
-
# a simple class rather than a full-blown Ractor with message passing, and it
|
|
26
|
-
# may also useful for adapting existing legacy object-based implementations.
|
|
87
|
+
# ## SQLite3 example
|
|
27
88
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# on which you can invoke methods. The wrapper responds to these method calls
|
|
31
|
-
# by sending messages to the internal Ractor, which invokes the shared object
|
|
32
|
-
# and then sends back the result. If the underlying object is thread-safe,
|
|
33
|
-
# you can configure the wrapper to run multiple threads that can run methods
|
|
34
|
-
# 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.
|
|
35
91
|
#
|
|
36
|
-
#
|
|
92
|
+
# require "ractor/wrapper"
|
|
93
|
+
# require "sqlite3"
|
|
37
94
|
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
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)
|
|
41
98
|
#
|
|
42
|
-
#
|
|
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)
|
|
43
104
|
#
|
|
44
|
-
# #
|
|
45
|
-
#
|
|
46
|
-
#
|
|
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")
|
|
47
108
|
#
|
|
48
|
-
# #
|
|
49
|
-
# #
|
|
50
|
-
#
|
|
109
|
+
# # You can also access the database via the stub object provided by the
|
|
110
|
+
# # wrapper.
|
|
111
|
+
# rows = wrapper.stub.execute("select * from numbers")
|
|
51
112
|
#
|
|
52
|
-
# #
|
|
53
|
-
# #
|
|
54
|
-
# #
|
|
55
|
-
# r1 = Ractor.new(wrapper) do |
|
|
56
|
-
#
|
|
57
|
-
#
|
|
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")
|
|
58
119
|
# end
|
|
59
120
|
# :ok
|
|
60
121
|
# end
|
|
61
|
-
# r2 = Ractor.new(wrapper) do |
|
|
62
|
-
#
|
|
63
|
-
#
|
|
122
|
+
# r2 = Ractor.new(wrapper.stub) do |db_stub|
|
|
123
|
+
# 5.times do
|
|
124
|
+
# rows = db_stub.execute("select * from numbers")
|
|
64
125
|
# end
|
|
65
126
|
# :ok
|
|
66
127
|
# end
|
|
67
128
|
#
|
|
68
129
|
# # Wait for the two above Ractors to finish.
|
|
69
|
-
# r1.
|
|
70
|
-
# r2.
|
|
130
|
+
# r1.join
|
|
131
|
+
# r2.join
|
|
71
132
|
#
|
|
72
|
-
# # After
|
|
73
|
-
# #
|
|
133
|
+
# # After stopping the wrapper, you can call the join method to wait for
|
|
134
|
+
# # it to completely finish.
|
|
74
135
|
# wrapper.async_stop
|
|
75
|
-
#
|
|
76
|
-
#
|
|
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
|
|
77
142
|
#
|
|
78
143
|
# ## Features
|
|
79
144
|
#
|
|
80
|
-
# * Provides a method interface to
|
|
145
|
+
# * Provides a Ractor-shareable method interface to a non-shareable object.
|
|
81
146
|
# * Supports arbitrary method arguments and return values.
|
|
82
|
-
# *
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
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.
|
|
87
155
|
# * Can gracefully shut down the wrapper and retrieve the original object.
|
|
88
156
|
#
|
|
89
157
|
# ## Caveats
|
|
90
158
|
#
|
|
91
|
-
# Ractor::Wrapper is subject to some limitations (and bugs) of Ractors, as of
|
|
92
|
-
# Ruby 3.0.0.
|
|
93
|
-
#
|
|
94
|
-
# * You cannot pass blocks to wrapped methods.
|
|
95
159
|
# * Certain types cannot be used as method arguments or return values
|
|
96
|
-
# because
|
|
97
|
-
# include threads,
|
|
98
|
-
# *
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
# *
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
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.
|
|
105
173
|
#
|
|
106
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
|
+
|
|
107
274
|
##
|
|
108
275
|
# Create a wrapper around the given object.
|
|
109
276
|
#
|
|
110
|
-
# If you pass an optional block, the wrapper itself will be yielded to it
|
|
111
|
-
# at which time you can set additional configuration options.
|
|
112
|
-
# 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.
|
|
113
281
|
#
|
|
114
282
|
# @param object [Object] The non-shareable object to wrap.
|
|
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.
|
|
115
288
|
# @param threads [Integer] The number of worker threads to run.
|
|
116
|
-
# Defaults to
|
|
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.
|
|
117
312
|
#
|
|
118
313
|
def initialize(object,
|
|
119
|
-
|
|
120
|
-
|
|
314
|
+
use_current_ractor: false,
|
|
315
|
+
name: nil,
|
|
316
|
+
threads: 0,
|
|
317
|
+
move_data: false,
|
|
121
318
|
move_arguments: nil,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
|
|
125
326
|
@method_settings = {}
|
|
327
|
+
self.name = name || object_id.to_s
|
|
328
|
+
self.enable_logging = enable_logging
|
|
126
329
|
self.threads = threads
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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)
|
|
130
336
|
yield self if block_given?
|
|
131
337
|
@method_settings.freeze
|
|
132
338
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
object
|
|
137
|
-
|
|
138
|
-
method_settings: @method_settings,
|
|
139
|
-
name: @name,
|
|
140
|
-
logging: @logging,
|
|
141
|
-
}
|
|
142
|
-
@ractor.send(opts, move: true)
|
|
143
|
-
|
|
144
|
-
maybe_log("Server ready")
|
|
339
|
+
if use_current_ractor
|
|
340
|
+
setup_local_server(object)
|
|
341
|
+
else
|
|
342
|
+
setup_isolated_server(object)
|
|
343
|
+
end
|
|
145
344
|
@stub = Stub.new(self)
|
|
345
|
+
|
|
146
346
|
freeze
|
|
147
347
|
end
|
|
148
348
|
|
|
149
349
|
##
|
|
150
350
|
# Set the number of threads to run in the wrapper. If the underlying object
|
|
151
|
-
# is thread-safe,
|
|
152
|
-
# object is not thread-safe, you should leave this
|
|
153
|
-
# which
|
|
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.
|
|
154
355
|
#
|
|
155
356
|
# This method can be called only during an initialization block.
|
|
156
357
|
# All settings are frozen once the wrapper is active.
|
|
@@ -159,7 +360,7 @@ class Ractor
|
|
|
159
360
|
#
|
|
160
361
|
def threads=(value)
|
|
161
362
|
value = value.to_i
|
|
162
|
-
value =
|
|
363
|
+
value = 0 if value.negative?
|
|
163
364
|
@threads = value
|
|
164
365
|
end
|
|
165
366
|
|
|
@@ -171,8 +372,8 @@ class Ractor
|
|
|
171
372
|
#
|
|
172
373
|
# @param value [Boolean]
|
|
173
374
|
#
|
|
174
|
-
def
|
|
175
|
-
@
|
|
375
|
+
def enable_logging=(value)
|
|
376
|
+
@enable_logging = value ? true : false
|
|
176
377
|
end
|
|
177
378
|
|
|
178
379
|
##
|
|
@@ -199,49 +400,73 @@ class Ractor
|
|
|
199
400
|
#
|
|
200
401
|
# @param method_name [Symbol, nil] The name of the method being configured,
|
|
201
402
|
# or `nil` to set defaults for all methods not configured explicitly.
|
|
202
|
-
# @param
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
# @param move_arguments [
|
|
206
|
-
#
|
|
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.
|
|
207
421
|
#
|
|
208
422
|
def configure_method(method_name = nil,
|
|
209
|
-
|
|
423
|
+
move_data: false,
|
|
210
424
|
move_arguments: nil,
|
|
211
|
-
|
|
425
|
+
move_results: nil,
|
|
426
|
+
move_block_arguments: nil,
|
|
427
|
+
move_block_results: nil,
|
|
428
|
+
execute_blocks_in_place: nil)
|
|
212
429
|
method_name = method_name.to_sym unless method_name.nil?
|
|
213
430
|
@method_settings[method_name] =
|
|
214
|
-
MethodSettings.new(
|
|
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)
|
|
215
437
|
end
|
|
216
438
|
|
|
217
439
|
##
|
|
218
|
-
# Return the
|
|
219
|
-
# methods as the wrapped object, providing an easy way to call a wrapper.
|
|
440
|
+
# Return the name of this wrapper.
|
|
220
441
|
#
|
|
221
|
-
# @return [
|
|
442
|
+
# @return [String]
|
|
222
443
|
#
|
|
223
|
-
attr_reader :
|
|
444
|
+
attr_reader :name
|
|
224
445
|
|
|
225
446
|
##
|
|
226
|
-
#
|
|
447
|
+
# Determine whether this wrapper runs in the current Ractor
|
|
227
448
|
#
|
|
228
|
-
# @return [
|
|
449
|
+
# @return [boolean]
|
|
229
450
|
#
|
|
230
|
-
|
|
451
|
+
def use_current_ractor?
|
|
452
|
+
@ractor.nil?
|
|
453
|
+
end
|
|
231
454
|
|
|
232
455
|
##
|
|
233
456
|
# Return whether logging is enabled for this wrapper.
|
|
234
457
|
#
|
|
235
458
|
# @return [Boolean]
|
|
236
459
|
#
|
|
237
|
-
|
|
460
|
+
def enable_logging?
|
|
461
|
+
@enable_logging
|
|
462
|
+
end
|
|
238
463
|
|
|
239
464
|
##
|
|
240
|
-
# Return the
|
|
465
|
+
# Return the number of worker threads used by the wrapper.
|
|
241
466
|
#
|
|
242
|
-
# @return [
|
|
467
|
+
# @return [Integer]
|
|
243
468
|
#
|
|
244
|
-
attr_reader :
|
|
469
|
+
attr_reader :threads
|
|
245
470
|
|
|
246
471
|
##
|
|
247
472
|
# Return the method settings for the given method name. This returns the
|
|
@@ -257,6 +482,14 @@ class Ractor
|
|
|
257
482
|
@method_settings[method_name] || @method_settings[nil]
|
|
258
483
|
end
|
|
259
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
|
|
492
|
+
|
|
260
493
|
##
|
|
261
494
|
# A lower-level interface for calling methods through the wrapper.
|
|
262
495
|
#
|
|
@@ -265,20 +498,34 @@ class Ractor
|
|
|
265
498
|
# @param kwargs [keywords] The keyword arguments
|
|
266
499
|
# @return [Object] The return value
|
|
267
500
|
#
|
|
268
|
-
def call(method_name, *args, **kwargs)
|
|
269
|
-
|
|
270
|
-
transaction =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
282
529
|
end
|
|
283
530
|
end
|
|
284
531
|
|
|
@@ -286,182 +533,256 @@ class Ractor
|
|
|
286
533
|
# Request that the wrapper stop. All currently running calls will complete
|
|
287
534
|
# before the wrapper actually terminates. However, any new calls will fail.
|
|
288
535
|
#
|
|
289
|
-
# This
|
|
536
|
+
# This method is idempotent and can be called multiple times (even from
|
|
290
537
|
# different ractors).
|
|
291
538
|
#
|
|
292
539
|
# @return [self]
|
|
293
540
|
#
|
|
294
541
|
def async_stop
|
|
295
|
-
maybe_log("Stopping
|
|
296
|
-
@
|
|
542
|
+
maybe_log("Stopping wrapper")
|
|
543
|
+
@port.send(StopMessage.new.freeze)
|
|
297
544
|
self
|
|
298
545
|
rescue ::Ractor::ClosedError
|
|
299
546
|
# Ignore to allow stops to be idempotent.
|
|
300
547
|
self
|
|
301
548
|
end
|
|
302
549
|
|
|
550
|
+
##
|
|
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
|
+
|
|
303
569
|
##
|
|
304
570
|
# Retrieves the original object that was wrapped. This should be called
|
|
305
571
|
# only after a stop request has been issued using {#async_stop}, and may
|
|
306
572
|
# block until the wrapper has fully stopped.
|
|
307
573
|
#
|
|
308
|
-
#
|
|
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.
|
|
309
581
|
#
|
|
310
582
|
# @return [Object] The original wrapped object
|
|
311
583
|
#
|
|
312
|
-
def
|
|
313
|
-
@ractor
|
|
584
|
+
def recover_object
|
|
585
|
+
raise ::Ractor::Error, "cannot recover an object from a local wrapper" unless @ractor
|
|
586
|
+
@ractor.value
|
|
314
587
|
end
|
|
315
588
|
|
|
316
|
-
private
|
|
589
|
+
#### private items below ####
|
|
317
590
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
end
|
|
591
|
+
##
|
|
592
|
+
# @private
|
|
593
|
+
# Message sent to initialize a server.
|
|
594
|
+
#
|
|
595
|
+
InitMessage = ::Data.define(:object, :enable_logging, :threads)
|
|
324
596
|
|
|
325
597
|
##
|
|
326
|
-
#
|
|
598
|
+
# @private
|
|
599
|
+
# Message sent to a server to call a method
|
|
327
600
|
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
# Create a stub given a wrapper.
|
|
331
|
-
#
|
|
332
|
-
# @param wrapper [Ractor::Wrapper]
|
|
333
|
-
#
|
|
334
|
-
def initialize(wrapper)
|
|
335
|
-
@wrapper = wrapper
|
|
336
|
-
freeze
|
|
337
|
-
end
|
|
601
|
+
CallMessage = ::Data.define(:method_name, :args, :kwargs, :block_arg,
|
|
602
|
+
:transaction, :settings, :reply_port)
|
|
338
603
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
@wrapper.call(name, *args, **kwargs)
|
|
345
|
-
end
|
|
604
|
+
##
|
|
605
|
+
# @private
|
|
606
|
+
# Message sent to a server when a worker thread terminates
|
|
607
|
+
#
|
|
608
|
+
WorkerStoppedMessage = ::Data.define(:worker_num)
|
|
346
609
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
@wrapper.call(:respond_to?, name, include_all)
|
|
353
|
-
end
|
|
354
|
-
end
|
|
610
|
+
##
|
|
611
|
+
# @private
|
|
612
|
+
# Message sent to a server to request it to stop
|
|
613
|
+
#
|
|
614
|
+
StopMessage = ::Data.define
|
|
355
615
|
|
|
356
616
|
##
|
|
357
|
-
#
|
|
358
|
-
#
|
|
617
|
+
# @private
|
|
618
|
+
# Message sent to a server to request a join response
|
|
359
619
|
#
|
|
360
|
-
|
|
361
|
-
# @private
|
|
362
|
-
def initialize(move: false,
|
|
363
|
-
move_arguments: nil,
|
|
364
|
-
move_return: nil)
|
|
365
|
-
@move_arguments = interpret_setting(move_arguments, move)
|
|
366
|
-
@move_return = interpret_setting(move_return, move)
|
|
367
|
-
freeze
|
|
368
|
-
end
|
|
620
|
+
JoinMessage = ::Data.define(:reply_port)
|
|
369
621
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
end
|
|
622
|
+
##
|
|
623
|
+
# @private
|
|
624
|
+
# Message sent to report a return value
|
|
625
|
+
#
|
|
626
|
+
ReturnMessage = ::Data.define(:value)
|
|
376
627
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
end
|
|
628
|
+
##
|
|
629
|
+
# @private
|
|
630
|
+
# Message sent to report an exception result
|
|
631
|
+
#
|
|
632
|
+
ExceptionMessage = ::Data.define(:exception)
|
|
383
633
|
|
|
384
|
-
|
|
634
|
+
##
|
|
635
|
+
# @private
|
|
636
|
+
# Message sent from a server to request a yield block run
|
|
637
|
+
#
|
|
638
|
+
YieldMessage = ::Data.define(:args, :kwargs, :reply_port)
|
|
385
639
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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)
|
|
392
656
|
end
|
|
393
657
|
end
|
|
394
658
|
|
|
395
659
|
##
|
|
396
|
-
#
|
|
397
|
-
# This
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
-
# Any Ractor that calls a wrapper may receive messages of this type when
|
|
401
|
-
# the call is in progress. If a Ractor interacts with its incoming message
|
|
402
|
-
# queue concurrently while a wrapped call is in progress, it must ignore
|
|
403
|
-
# these messages (i.e. by by using `receive_if`) in order not to interfere
|
|
404
|
-
# with the wrapper. (Similarly, the wrapper will use `receive_if` to
|
|
405
|
-
# receive only messages of this type, so it does not interfere with your
|
|
406
|
-
# Ractor's functionality.)
|
|
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.
|
|
407
663
|
#
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
@transaction = transaction || new_transaction
|
|
415
|
-
freeze
|
|
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)
|
|
416
670
|
end
|
|
671
|
+
@port = @ractor.default_port
|
|
672
|
+
@port.send(object, move: true)
|
|
673
|
+
end
|
|
417
674
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
# @private
|
|
425
|
-
attr_reader :transaction
|
|
426
|
-
|
|
427
|
-
# @private
|
|
428
|
-
attr_reader :data
|
|
429
|
-
|
|
430
|
-
private
|
|
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
|
|
431
681
|
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
434
692
|
end
|
|
435
693
|
end
|
|
436
694
|
|
|
437
695
|
##
|
|
438
|
-
#
|
|
439
|
-
# Ractor, and manages a shared object. It handles communication with
|
|
440
|
-
# clients, translating those messages into method calls on the object. It
|
|
441
|
-
# runs worker threads internally to handle actual method calls.
|
|
696
|
+
# Handle a call to a block directed to run in the caller environment.
|
|
442
697
|
#
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
##
|
|
719
|
+
# Prints out a log message
|
|
445
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
|
+
##
|
|
446
732
|
# @private
|
|
447
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
|
+
#
|
|
448
743
|
class Server
|
|
449
744
|
##
|
|
450
|
-
#
|
|
745
|
+
# @private
|
|
746
|
+
# Create and run a server hosted in the current Ractor
|
|
451
747
|
#
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
456
756
|
#
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
762
|
+
|
|
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
|
|
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.
|
|
459
779
|
#
|
|
460
780
|
def run
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
781
|
+
receive_remote_object if @isolated
|
|
782
|
+
start_workers if @threads
|
|
783
|
+
main_loop
|
|
784
|
+
stop_workers if @threads
|
|
785
|
+
cleanup
|
|
465
786
|
@object
|
|
466
787
|
rescue ::StandardError => e
|
|
467
788
|
maybe_log("Unexpected error: #{e.inspect}")
|
|
@@ -471,200 +792,259 @@ class Ractor
|
|
|
471
792
|
private
|
|
472
793
|
|
|
473
794
|
##
|
|
474
|
-
#
|
|
475
|
-
#
|
|
476
|
-
# * Receives an initial message providing the object to wrap, and
|
|
477
|
-
# server configuration such as thread count and communications
|
|
478
|
-
# settings.
|
|
479
|
-
# * Initializes the job queue and the pending request list.
|
|
480
|
-
# * Spawns worker threads.
|
|
795
|
+
# Receive the moved remote object. Called if the server is run in a
|
|
796
|
+
# separate Ractor.
|
|
481
797
|
#
|
|
482
|
-
def
|
|
483
|
-
|
|
484
|
-
@object =
|
|
485
|
-
@logging = opts[:logging]
|
|
486
|
-
@name = opts[:name]
|
|
487
|
-
@method_settings = opts[:method_settings]
|
|
488
|
-
@thread_count = opts[:threads]
|
|
489
|
-
@queue = ::Queue.new
|
|
490
|
-
@mutex = ::Mutex.new
|
|
491
|
-
@current_calls = {}
|
|
492
|
-
maybe_log("Spawning #{@thread_count} threads")
|
|
493
|
-
(1..@thread_count).map do |worker_num|
|
|
494
|
-
::Thread.new { worker_thread(worker_num) }
|
|
495
|
-
end
|
|
496
|
-
maybe_log("Server initialized")
|
|
798
|
+
def receive_remote_object
|
|
799
|
+
maybe_log("Waiting for remote object")
|
|
800
|
+
@object = @port.receive
|
|
497
801
|
end
|
|
498
802
|
|
|
499
803
|
##
|
|
500
|
-
#
|
|
501
|
-
# queue
|
|
502
|
-
# request from the pending request list to signal that it has responded.
|
|
503
|
-
# If no job is available, the thread blocks while waiting. If the queue
|
|
504
|
-
# is closed, the worker will send an acknowledgement message and then
|
|
505
|
-
# terminate.
|
|
804
|
+
# Start the worker threads. Each thread picks up methods to run from a
|
|
805
|
+
# shared queue. Called only if worker threading is enabled.
|
|
506
806
|
#
|
|
507
|
-
def
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
break if request.nil?
|
|
513
|
-
handle_method(worker_num, request)
|
|
514
|
-
unregister_call(request.transaction)
|
|
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) }
|
|
515
812
|
end
|
|
516
|
-
ensure
|
|
517
|
-
maybe_worker_log(worker_num, "Stopping")
|
|
518
|
-
::Ractor.current.send(Message.new(:thread_stopped, data: worker_num), move: true)
|
|
519
813
|
end
|
|
520
814
|
|
|
521
815
|
##
|
|
522
|
-
#
|
|
523
|
-
#
|
|
816
|
+
# This is the main loop, listening on the inbox and handling messages for
|
|
817
|
+
# normal operation:
|
|
524
818
|
#
|
|
525
|
-
# * If it receives a
|
|
526
|
-
#
|
|
527
|
-
#
|
|
528
|
-
#
|
|
529
|
-
# * If it receives a
|
|
530
|
-
#
|
|
531
|
-
#
|
|
532
|
-
#
|
|
533
|
-
# stopping
|
|
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.
|
|
534
829
|
#
|
|
535
|
-
def
|
|
830
|
+
def main_loop
|
|
536
831
|
loop do
|
|
537
|
-
maybe_log("Waiting for message")
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
@
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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)
|
|
839
|
+
else
|
|
840
|
+
handle_method(message)
|
|
841
|
+
end
|
|
842
|
+
when WorkerStoppedMessage
|
|
843
|
+
maybe_log("Received unexpected WorkerStoppedMessage")
|
|
844
|
+
@threads -= 1 if @threads
|
|
548
845
|
break
|
|
549
|
-
when
|
|
846
|
+
when StopMessage
|
|
550
847
|
maybe_log("Received stop")
|
|
551
848
|
break
|
|
849
|
+
when JoinMessage
|
|
850
|
+
maybe_log("Received and queueing join request")
|
|
851
|
+
@join_requests << message.reply_port
|
|
552
852
|
end
|
|
553
853
|
end
|
|
554
854
|
end
|
|
555
855
|
|
|
556
856
|
##
|
|
557
|
-
#
|
|
558
|
-
#
|
|
559
|
-
#
|
|
560
|
-
#
|
|
561
|
-
#
|
|
562
|
-
#
|
|
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.
|
|
563
873
|
#
|
|
564
|
-
|
|
874
|
+
# This phase continues until all workers have signaled that they have
|
|
875
|
+
# stopped.
|
|
876
|
+
#
|
|
877
|
+
def stop_workers
|
|
565
878
|
@queue.close
|
|
566
|
-
while @
|
|
567
|
-
maybe_log("Waiting for message
|
|
568
|
-
message =
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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")
|
|
907
|
+
loop do
|
|
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
|
|
572
917
|
refuse_method(message)
|
|
573
|
-
when
|
|
574
|
-
|
|
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)
|
|
575
925
|
end
|
|
576
926
|
end
|
|
577
927
|
end
|
|
578
928
|
|
|
579
929
|
##
|
|
580
|
-
#
|
|
581
|
-
#
|
|
582
|
-
#
|
|
583
|
-
#
|
|
584
|
-
#
|
|
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.
|
|
585
936
|
#
|
|
586
|
-
def
|
|
587
|
-
|
|
588
|
-
maybe_log("Checking message queue for cleanup")
|
|
937
|
+
def worker_thread(worker_num)
|
|
938
|
+
maybe_log("Worker starting", worker_num: worker_num)
|
|
589
939
|
loop do
|
|
590
|
-
|
|
591
|
-
|
|
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)
|
|
592
944
|
end
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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)
|
|
596
951
|
end
|
|
597
|
-
rescue ::Ractor::ClosedError
|
|
598
|
-
maybe_log("Message queue is empty")
|
|
599
952
|
end
|
|
600
953
|
|
|
601
954
|
##
|
|
602
|
-
# This is called
|
|
955
|
+
# This is called to handle a method call request.
|
|
603
956
|
# It calls the method on the wrapped object, and then sends back a
|
|
604
957
|
# response to the caller. If an exception was raised, it sends back an
|
|
605
958
|
# error response. It tries very hard always to send a response of some
|
|
606
959
|
# kind; if an error occurs while constructing or sending a response, it
|
|
607
|
-
# will catch the exception and try to send a simpler response.
|
|
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.
|
|
608
962
|
#
|
|
609
|
-
def handle_method(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
sender = request.sender
|
|
613
|
-
maybe_worker_log(worker_num, "Running method #{method_name} (transaction=#{transaction})")
|
|
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)
|
|
614
966
|
begin
|
|
615
|
-
result = @object.
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
move: (@method_settings[method_name] || @method_settings[nil]).move_return?)
|
|
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?)
|
|
619
970
|
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
620
|
-
|
|
971
|
+
maybe_log("Sending exception", worker_num: worker_num, call_message: message)
|
|
621
972
|
begin
|
|
622
|
-
|
|
973
|
+
message.reply_port.send(ExceptionMessage.new(e))
|
|
623
974
|
rescue ::StandardError
|
|
624
|
-
|
|
625
|
-
::StandardError.new(e.inspect)
|
|
975
|
+
begin
|
|
976
|
+
message.reply_port.send(ExceptionMessage.new(::StandardError.new(e.inspect)))
|
|
626
977
|
rescue ::StandardError
|
|
627
|
-
|
|
978
|
+
maybe_log("Failure to send method response", worker_num: worker_num, call_message: message)
|
|
628
979
|
end
|
|
629
|
-
sender.send(Message.new(:error, data: safe_error, transaction: transaction))
|
|
630
980
|
end
|
|
631
981
|
end
|
|
632
982
|
end
|
|
633
983
|
|
|
634
984
|
##
|
|
635
|
-
#
|
|
636
|
-
# the
|
|
637
|
-
# wrapper is shutting down.
|
|
985
|
+
# Creates a block appropriate to the block specification received with
|
|
986
|
+
# the method call message. This could return:
|
|
638
987
|
#
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
def
|
|
646
|
-
|
|
647
|
-
|
|
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
|
|
648
1008
|
end
|
|
649
1009
|
end
|
|
650
1010
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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)
|
|
654
1023
|
end
|
|
655
1024
|
end
|
|
656
1025
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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")
|
|
662
1033
|
end
|
|
663
1034
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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}")
|
|
668
1048
|
$stderr.flush
|
|
669
1049
|
end
|
|
670
1050
|
end
|