ractor-wrapper 0.2.0 → 0.4.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 +31 -0
- data/CLAUDE.md +76 -0
- data/LICENSE.md +1 -1
- data/README.md +423 -88
- data/lib/ractor/wrapper/version.rb +3 -1
- data/lib/ractor/wrapper.rb +1071 -442
- data/lib/ractor-wrapper.rb +2 -0
- metadata +9 -12
data/lib/ractor/wrapper.rb
CHANGED
|
@@ -1,247 +1,595 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
##
|
|
2
|
-
# See ruby-
|
|
4
|
+
# See https://docs.ruby-lang.org/en/4.0/language/ractor_md.html for info on
|
|
5
|
+
# Ractors.
|
|
3
6
|
#
|
|
4
7
|
class Ractor
|
|
5
8
|
##
|
|
6
|
-
# An experimental class that wraps a non-shareable object
|
|
7
|
-
# Ractors to access it concurrently.
|
|
9
|
+
# An experimental class that wraps a non-shareable object in an actor,
|
|
10
|
+
# allowing multiple Ractors to access it concurrently.
|
|
8
11
|
#
|
|
9
12
|
# WARNING: This is a highly experimental library, and currently _not_
|
|
10
|
-
# recommended for production use. (As of Ruby
|
|
13
|
+
# recommended for production use. (As of Ruby 4.0.0, the same can be said of
|
|
11
14
|
# Ractors in general.)
|
|
12
15
|
#
|
|
13
16
|
# ## What is Ractor::Wrapper?
|
|
14
17
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
18
|
+
# For the most part, unless an object is _sharable_, which generally means
|
|
19
|
+
# deeply immutable along with a few other restrictions, it cannot be accessed
|
|
20
|
+
# directly from another Ractor. This makes it difficult for multiple Ractors
|
|
21
|
+
# to share a resource that is stateful. Such a resource must typically itself
|
|
22
|
+
# be implemented as a Ractor and accessed via message passing.
|
|
23
|
+
#
|
|
24
|
+
# Ractor::Wrapper makes it possible for an ordinary non-shareable object to
|
|
25
|
+
# be accessed from multiple Ractors. It does this by "wrapping" the object
|
|
26
|
+
# with an actor that listens for messages and invokes the object's methods in
|
|
27
|
+
# a controlled single-Ractor environment. It then provides a stub object that
|
|
28
|
+
# reproduces the interface of the original object, but responds to method
|
|
29
|
+
# calls by sending messages to the wrapper. Ractor::Wrapper can be used to
|
|
30
|
+
# implement simple actors by writing "plain" Ruby objects, or to adapt
|
|
31
|
+
# existing non-shareable objects to a multi-Ractor world.
|
|
32
|
+
#
|
|
33
|
+
# ## Net::HTTP example
|
|
34
|
+
#
|
|
35
|
+
# The following example shows how to share a single Net::HTTP session object
|
|
36
|
+
# among multiple Ractors.
|
|
37
|
+
#
|
|
38
|
+
# require "ractor/wrapper"
|
|
39
|
+
# require "net/http"
|
|
40
|
+
#
|
|
41
|
+
# # Create a Net::HTTP session. Net::HTTP sessions are not shareable,
|
|
42
|
+
# # so normally only one Ractor can access them at a time.
|
|
43
|
+
# http = Net::HTTP.new("example.com")
|
|
44
|
+
# http.start
|
|
45
|
+
#
|
|
46
|
+
# # Create a wrapper around the session. This moves the session into an
|
|
47
|
+
# # internal Ractor and listens for method call requests. By default, a
|
|
48
|
+
# # wrapper serializes calls, handling one at a time, for compatibility
|
|
49
|
+
# # with non-thread-safe objects.
|
|
50
|
+
# wrapper = Ractor::Wrapper.new(http)
|
|
51
|
+
#
|
|
52
|
+
# # At this point, the session object can no longer be accessed directly
|
|
53
|
+
# # because it is now owned by the wrapper's internal Ractor.
|
|
54
|
+
# # http.get("/whoops") # <= raises Ractor::MovedError
|
|
55
|
+
#
|
|
56
|
+
# # However, you can access the session via the stub object provided by
|
|
57
|
+
# # the wrapper. This stub proxies the call to the wrapper's internal
|
|
58
|
+
# # Ractor. And it's shareable, so any number of Ractors can use it.
|
|
59
|
+
# response = wrapper.stub.get("/")
|
|
60
|
+
#
|
|
61
|
+
# # Here, we start two Ractors, and pass the stub to each one. Each
|
|
62
|
+
# # Ractor can simply call methods on the stub as if it were the original
|
|
63
|
+
# # connection object. Internally, of course, the calls are proxied to
|
|
64
|
+
# # the original object via the wrapper, and execution is serialized.
|
|
65
|
+
# r1 = Ractor.new(wrapper.stub) do |stub|
|
|
66
|
+
# 5.times do
|
|
67
|
+
# stub.get("/hello")
|
|
68
|
+
# end
|
|
69
|
+
# :ok
|
|
70
|
+
# end
|
|
71
|
+
# r2 = Ractor.new(wrapper.stub) do |stub|
|
|
72
|
+
# 5.times do
|
|
73
|
+
# stub.get("/ruby")
|
|
74
|
+
# end
|
|
75
|
+
# :ok
|
|
76
|
+
# end
|
|
77
|
+
#
|
|
78
|
+
# # Wait for the two above Ractors to finish.
|
|
79
|
+
# r1.join
|
|
80
|
+
# r2.join
|
|
81
|
+
#
|
|
82
|
+
# # After you stop the wrapper, you can retrieve the underlying session
|
|
83
|
+
# # object and access it directly again.
|
|
84
|
+
# wrapper.async_stop
|
|
85
|
+
# http = wrapper.recover_object
|
|
86
|
+
# http.finish
|
|
20
87
|
#
|
|
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.
|
|
88
|
+
# ## SQLite3 example
|
|
27
89
|
#
|
|
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.
|
|
90
|
+
# The following example shows how to share a SQLite3 database among multiple
|
|
91
|
+
# Ractors.
|
|
35
92
|
#
|
|
36
|
-
#
|
|
93
|
+
# require "ractor/wrapper"
|
|
94
|
+
# require "sqlite3"
|
|
37
95
|
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
96
|
+
# # Create a SQLite3 database. These objects are not shareable, so
|
|
97
|
+
# # normally only one Ractor can access them.
|
|
98
|
+
# db = SQLite3::Database.new($my_database_path)
|
|
41
99
|
#
|
|
42
|
-
#
|
|
100
|
+
# # Create a wrapper around the database. A SQLite3::Database object
|
|
101
|
+
# # cannot be moved between Ractors, so we configure the wrapper to run
|
|
102
|
+
# # in the current Ractor. We can also configure it to run multiple
|
|
103
|
+
# # worker threads because the database object itself is thread-safe.
|
|
104
|
+
# wrapper = Ractor::Wrapper.new(db, use_current_ractor: true, threads: 2)
|
|
43
105
|
#
|
|
44
|
-
# #
|
|
45
|
-
#
|
|
46
|
-
#
|
|
106
|
+
# # At this point, the database object can still be accessed directly
|
|
107
|
+
# # because it hasn't been moved to a different Ractor.
|
|
108
|
+
# rows = db.execute("select * from numbers")
|
|
47
109
|
#
|
|
48
|
-
# #
|
|
49
|
-
# #
|
|
50
|
-
#
|
|
110
|
+
# # You can also access the database via the stub object provided by the
|
|
111
|
+
# # wrapper.
|
|
112
|
+
# rows = wrapper.stub.execute("select * from numbers")
|
|
51
113
|
#
|
|
52
|
-
# #
|
|
53
|
-
# #
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
# w.stub.get("/hello")
|
|
114
|
+
# # Here, we start two Ractors, and pass the stub to each one. The
|
|
115
|
+
# # wrapper's worker threads will handle the requests concurrently.
|
|
116
|
+
# r1 = Ractor.new(wrapper.stub) do |stub|
|
|
117
|
+
# 5.times do
|
|
118
|
+
# 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 |stub|
|
|
123
|
+
# 5.times do
|
|
124
|
+
# 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::Wrapper::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
|
|
107
175
|
##
|
|
108
|
-
#
|
|
176
|
+
# Base class for errors raised by {Ractor::Wrapper}.
|
|
109
177
|
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
#
|
|
114
|
-
# @param object [Object] The non-shareable object to wrap.
|
|
115
|
-
# @param threads [Integer] The number of worker threads to run.
|
|
116
|
-
# Defaults to 1, which causes the worker to serialize calls.
|
|
178
|
+
class Error < ::Ractor::Error; end
|
|
179
|
+
|
|
180
|
+
##
|
|
181
|
+
# Raised when a {Ractor::Wrapper} server has crashed unexpectedly.
|
|
117
182
|
#
|
|
118
|
-
|
|
119
|
-
threads: 1,
|
|
120
|
-
move: false,
|
|
121
|
-
move_arguments: nil,
|
|
122
|
-
move_return: nil,
|
|
123
|
-
logging: false,
|
|
124
|
-
name: nil)
|
|
125
|
-
@method_settings = {}
|
|
126
|
-
self.threads = threads
|
|
127
|
-
self.logging = logging
|
|
128
|
-
self.name = name
|
|
129
|
-
configure_method(move: move, move_arguments: move_arguments, move_return: move_return)
|
|
130
|
-
yield self if block_given?
|
|
131
|
-
@method_settings.freeze
|
|
132
|
-
|
|
133
|
-
maybe_log("Starting server")
|
|
134
|
-
@ractor = ::Ractor.new(name: name) { Server.new.run }
|
|
135
|
-
opts = {
|
|
136
|
-
object: object,
|
|
137
|
-
threads: @threads,
|
|
138
|
-
method_settings: @method_settings,
|
|
139
|
-
name: @name,
|
|
140
|
-
logging: @logging,
|
|
141
|
-
}
|
|
142
|
-
@ractor.send(opts, move: true)
|
|
143
|
-
|
|
144
|
-
maybe_log("Server ready")
|
|
145
|
-
@stub = Stub.new(self)
|
|
146
|
-
freeze
|
|
147
|
-
end
|
|
183
|
+
class CrashedError < Error; end
|
|
148
184
|
|
|
149
185
|
##
|
|
150
|
-
#
|
|
151
|
-
# is
|
|
152
|
-
# object is not thread-safe, you should leave this set to its default of 1,
|
|
153
|
-
# which effectively causes calls to be serialized.
|
|
186
|
+
# Raised when calling a method on a {Ractor::Wrapper} whose server has
|
|
187
|
+
# stopped and is no longer accepting calls.
|
|
154
188
|
#
|
|
155
|
-
|
|
156
|
-
|
|
189
|
+
class StoppedError < Error; end
|
|
190
|
+
|
|
191
|
+
##
|
|
192
|
+
# A stub that forwards calls to a wrapper.
|
|
157
193
|
#
|
|
158
|
-
#
|
|
194
|
+
# This object is shareable and can be passed to any Ractor.
|
|
159
195
|
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
196
|
+
class Stub
|
|
197
|
+
##
|
|
198
|
+
# Create a stub given a wrapper.
|
|
199
|
+
#
|
|
200
|
+
# @param wrapper [Ractor::Wrapper]
|
|
201
|
+
#
|
|
202
|
+
def initialize(wrapper)
|
|
203
|
+
@wrapper = wrapper
|
|
204
|
+
freeze
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
##
|
|
208
|
+
# Forward calls to {Ractor::Wrapper#call}.
|
|
209
|
+
# @private
|
|
210
|
+
#
|
|
211
|
+
def method_missing(name, ...)
|
|
212
|
+
@wrapper.call(name, ...)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
##
|
|
216
|
+
# Forward respond_to queries.
|
|
217
|
+
# @private
|
|
218
|
+
#
|
|
219
|
+
def respond_to_missing?(name, include_all)
|
|
220
|
+
@wrapper.call(:respond_to?, name, include_all)
|
|
221
|
+
end
|
|
164
222
|
end
|
|
165
223
|
|
|
166
224
|
##
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
225
|
+
# Configuration for a {Ractor::Wrapper}. An instance of this class is
|
|
226
|
+
# yielded by {Ractor::Wrapper#initialize} if a block is provided. Any
|
|
227
|
+
# settings made to the Configuration before the block returns take
|
|
228
|
+
# effect when the Wrapper is constructed.
|
|
171
229
|
#
|
|
172
|
-
|
|
230
|
+
class Configuration
|
|
231
|
+
##
|
|
232
|
+
# Set the name of the wrapper. This is shown in logging and is also
|
|
233
|
+
# used as the name of the wrapping Ractor.
|
|
234
|
+
#
|
|
235
|
+
# @param value [String, nil]
|
|
236
|
+
#
|
|
237
|
+
def name=(value)
|
|
238
|
+
@name = value ? value.to_s.freeze : nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
##
|
|
242
|
+
# Enable or disable internal debug logging.
|
|
243
|
+
#
|
|
244
|
+
# @param value [Boolean]
|
|
245
|
+
#
|
|
246
|
+
def enable_logging=(value)
|
|
247
|
+
@enable_logging = value ? true : false
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
##
|
|
251
|
+
# Set the number of worker threads. If the underlying object is
|
|
252
|
+
# thread-safe, a value of 2 or more allows concurrent calls. Leave at
|
|
253
|
+
# the default of 0 to handle calls sequentially without worker threads.
|
|
254
|
+
#
|
|
255
|
+
# @param value [Integer]
|
|
256
|
+
#
|
|
257
|
+
def threads=(value)
|
|
258
|
+
value = value.to_i
|
|
259
|
+
value = 0 if value.negative?
|
|
260
|
+
@threads = value
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
##
|
|
264
|
+
# If set to true, the wrapper server runs as Thread(s) inside the
|
|
265
|
+
# current Ractor rather than spawning a new isolated Ractor. Use this
|
|
266
|
+
# for objects that cannot be moved between Ractors.
|
|
267
|
+
#
|
|
268
|
+
# @param value [Boolean]
|
|
269
|
+
#
|
|
270
|
+
def use_current_ractor=(value)
|
|
271
|
+
@use_current_ractor = value ? true : false
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
##
|
|
275
|
+
# Configure how argument and return values are communicated for the given
|
|
276
|
+
# method.
|
|
277
|
+
#
|
|
278
|
+
# In general, the following values are recognized for the data-moving
|
|
279
|
+
# settings:
|
|
280
|
+
#
|
|
281
|
+
# * `:copy` - Method arguments or return values that are not shareable,
|
|
282
|
+
# are *deep copied* when communicated between the caller and the object.
|
|
283
|
+
# * `:move` - Method arguments or return values that are not shareable,
|
|
284
|
+
# are *moved* when communicated between the caller and the object. This
|
|
285
|
+
# means they are no longer available to the source; that is, the caller
|
|
286
|
+
# can no longer access objects that were moved to method arguments, and
|
|
287
|
+
# the wrapped object can no longer access objects that were used as
|
|
288
|
+
# return values.
|
|
289
|
+
# * `:void` - This option is available for return values and block
|
|
290
|
+
# results. It disables return values for the given method, and is
|
|
291
|
+
# intended to avoid copying or moving objects that are not intended to
|
|
292
|
+
# be return values. The recipient will receive `nil`.
|
|
293
|
+
#
|
|
294
|
+
# The following settings are recognized for the `block_environment`
|
|
295
|
+
# setting:
|
|
296
|
+
#
|
|
297
|
+
# * `:caller` - Blocks are executed in the caller's context. This means
|
|
298
|
+
# the wrapper sends a message back to the caller to execute the block
|
|
299
|
+
# in its original context. This means the block will have access to its
|
|
300
|
+
# lexical scope and any other data available to the calling Ractor.
|
|
301
|
+
# * `:wrapped` - Blocks are executed directly in the wrapped object's
|
|
302
|
+
# context. This does not require any communication, but it means the
|
|
303
|
+
# block is removed from the caller's environment and does not have
|
|
304
|
+
# access to the caller's lexical scope or Ractor-accessible data.
|
|
305
|
+
#
|
|
306
|
+
# All settings are optional. If not provided, they will fall back to a
|
|
307
|
+
# default. If you are configuring a particular method, by specifying the
|
|
308
|
+
# `method_name` argument, any unspecified setting will fall back to the
|
|
309
|
+
# method default settings (which you can set by omitting the method name.)
|
|
310
|
+
# If you are configuring the method default settings, by omitting the
|
|
311
|
+
# `method_name` argument, unspecified settings will fall back to `:copy`
|
|
312
|
+
# for the data movement settings, and `:caller` for the
|
|
313
|
+
# `block_environment` setting.
|
|
314
|
+
#
|
|
315
|
+
# @param method_name [Symbol,nil] The name of the method being configured,
|
|
316
|
+
# or `nil` to set defaults for all methods not configured explicitly.
|
|
317
|
+
# @param arguments [:move,:copy] How to communicate method arguments.
|
|
318
|
+
# @param results [:move,:copy,:void] How to communicate method return
|
|
319
|
+
# values.
|
|
320
|
+
# @param block_arguments [:move,:copy] How to communicate block arguments.
|
|
321
|
+
# @param block_results [:move,:copy,:void] How to communicate block
|
|
322
|
+
# result values.
|
|
323
|
+
# @param block_environment [:caller,:wrapped] How to execute blocks, and
|
|
324
|
+
# what scope blocks have access to.
|
|
325
|
+
#
|
|
326
|
+
def configure_method(method_name = nil,
|
|
327
|
+
arguments: nil,
|
|
328
|
+
results: nil,
|
|
329
|
+
block_arguments: nil,
|
|
330
|
+
block_results: nil,
|
|
331
|
+
block_environment: nil)
|
|
332
|
+
method_name = method_name.to_sym unless method_name.nil?
|
|
333
|
+
@method_settings[method_name] =
|
|
334
|
+
MethodSettings.new(arguments: arguments,
|
|
335
|
+
results: results,
|
|
336
|
+
block_arguments: block_arguments,
|
|
337
|
+
block_results: block_results,
|
|
338
|
+
block_environment: block_environment)
|
|
339
|
+
self
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
##
|
|
343
|
+
# @private
|
|
344
|
+
# Return the name of the wrapper.
|
|
345
|
+
#
|
|
346
|
+
# @return [String, nil]
|
|
347
|
+
#
|
|
348
|
+
attr_reader :name
|
|
349
|
+
|
|
350
|
+
##
|
|
351
|
+
# @private
|
|
352
|
+
# Return whether logging is enabled.
|
|
353
|
+
#
|
|
354
|
+
# @return [Boolean]
|
|
355
|
+
#
|
|
356
|
+
attr_reader :enable_logging
|
|
357
|
+
|
|
358
|
+
##
|
|
359
|
+
# @private
|
|
360
|
+
# Return the number of worker threads.
|
|
361
|
+
#
|
|
362
|
+
# @return [Integer]
|
|
363
|
+
#
|
|
364
|
+
attr_reader :threads
|
|
365
|
+
|
|
366
|
+
##
|
|
367
|
+
# @private
|
|
368
|
+
# Return whether the wrapper runs in the current Ractor.
|
|
369
|
+
#
|
|
370
|
+
# @return [Boolean]
|
|
371
|
+
#
|
|
372
|
+
attr_reader :use_current_ractor
|
|
373
|
+
|
|
374
|
+
##
|
|
375
|
+
# @private
|
|
376
|
+
# Resolve the method settings by filling in the defaults for all fields
|
|
377
|
+
# not explicitly set, and return the final settings keyed by method name.
|
|
378
|
+
# The `nil` key will contain defaults for method names not explicitly
|
|
379
|
+
# configured. This hash will be frozen and shareable.
|
|
380
|
+
#
|
|
381
|
+
# @return [Hash{(Symbol,nil)=>MethodSettings}]
|
|
382
|
+
#
|
|
383
|
+
def final_method_settings
|
|
384
|
+
fallback = MethodSettings.new(arguments: :copy, results: :copy,
|
|
385
|
+
block_arguments: :copy, block_results: :copy,
|
|
386
|
+
block_environment: :caller)
|
|
387
|
+
defaults = MethodSettings.with_fallback(@method_settings[nil], fallback)
|
|
388
|
+
results = {nil => defaults}
|
|
389
|
+
@method_settings.each do |name, settings|
|
|
390
|
+
next if name.nil?
|
|
391
|
+
results[name] = MethodSettings.with_fallback(settings, defaults)
|
|
392
|
+
end
|
|
393
|
+
results.freeze
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
##
|
|
397
|
+
# @private
|
|
398
|
+
# Create an empty configuration.
|
|
399
|
+
#
|
|
400
|
+
def initialize
|
|
401
|
+
@method_settings = {}
|
|
402
|
+
configure_method(arguments: nil,
|
|
403
|
+
results: nil,
|
|
404
|
+
block_arguments: nil,
|
|
405
|
+
block_results: nil,
|
|
406
|
+
block_environment: nil)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
##
|
|
411
|
+
# Settings for a method call. Specifies how a method's arguments and
|
|
412
|
+
# return value are communicated (i.e. copy or move semantics.)
|
|
173
413
|
#
|
|
174
|
-
|
|
175
|
-
@
|
|
414
|
+
class MethodSettings
|
|
415
|
+
# @private
|
|
416
|
+
def initialize(arguments: nil,
|
|
417
|
+
results: nil,
|
|
418
|
+
block_arguments: nil,
|
|
419
|
+
block_results: nil,
|
|
420
|
+
block_environment: nil)
|
|
421
|
+
unless [nil, :copy, :move].include?(arguments)
|
|
422
|
+
raise ::ArgumentError, "Unknown `arguments`: #{arguments.inspect} (must be :copy or :move)"
|
|
423
|
+
end
|
|
424
|
+
unless [nil, :copy, :move, :void].include?(results)
|
|
425
|
+
raise ::ArgumentError, "Unknown `results`: #{results.inspect} (must be :copy, :move, or :void)"
|
|
426
|
+
end
|
|
427
|
+
unless [nil, :copy, :move].include?(block_arguments)
|
|
428
|
+
raise ::ArgumentError, "Unknown `block_arguments`: #{block_arguments.inspect} (must be :copy or :move)"
|
|
429
|
+
end
|
|
430
|
+
unless [nil, :copy, :move, :void].include?(block_results)
|
|
431
|
+
raise ::ArgumentError, "Unknown `block_results`: #{block_results.inspect} (must be :copy, :move, or :void)"
|
|
432
|
+
end
|
|
433
|
+
unless [nil, :caller, :wrapped].include?(block_environment)
|
|
434
|
+
raise ::ArgumentError,
|
|
435
|
+
"Unknown `block_environment`: #{block_environment.inspect} (must be :caller or :wrapped)"
|
|
436
|
+
end
|
|
437
|
+
@arguments = arguments
|
|
438
|
+
@results = results
|
|
439
|
+
@block_arguments = block_arguments
|
|
440
|
+
@block_results = block_results
|
|
441
|
+
@block_environment = block_environment
|
|
442
|
+
freeze
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
##
|
|
446
|
+
# @return [:copy,:move] How to communicate method arguments
|
|
447
|
+
# @return [nil] if not set (will not happen in final settings)
|
|
448
|
+
#
|
|
449
|
+
attr_reader :arguments
|
|
450
|
+
|
|
451
|
+
##
|
|
452
|
+
# @return [:copy,:move,:void] How to communicate method return values
|
|
453
|
+
# @return [nil] if not set (will not happen in final settings)
|
|
454
|
+
#
|
|
455
|
+
attr_reader :results
|
|
456
|
+
|
|
457
|
+
##
|
|
458
|
+
# @return [:copy,:move] How to communicate arguments to a block
|
|
459
|
+
# @return [nil] if not set (will not happen in final settings)
|
|
460
|
+
#
|
|
461
|
+
attr_reader :block_arguments
|
|
462
|
+
|
|
463
|
+
##
|
|
464
|
+
# @return [:copy,:move,:void] How to communicate block results
|
|
465
|
+
# @return [nil] if not set (will not happen in final settings)
|
|
466
|
+
#
|
|
467
|
+
attr_reader :block_results
|
|
468
|
+
|
|
469
|
+
##
|
|
470
|
+
# @return [:caller,:wrapped] What environment blocks execute in
|
|
471
|
+
# @return [nil] if not set (will not happen in final settings)
|
|
472
|
+
#
|
|
473
|
+
attr_reader :block_environment
|
|
474
|
+
|
|
475
|
+
# @private
|
|
476
|
+
def self.with_fallback(settings, fallback)
|
|
477
|
+
new(
|
|
478
|
+
arguments: settings.arguments || fallback.arguments,
|
|
479
|
+
results: settings.results || fallback.results,
|
|
480
|
+
block_arguments: settings.block_arguments || fallback.block_arguments,
|
|
481
|
+
block_results: settings.block_results || fallback.block_results,
|
|
482
|
+
block_environment: settings.block_environment || fallback.block_environment
|
|
483
|
+
)
|
|
484
|
+
end
|
|
176
485
|
end
|
|
177
486
|
|
|
178
487
|
##
|
|
179
|
-
#
|
|
180
|
-
# as the name of the wrapping Ractor.
|
|
488
|
+
# Create a wrapper around the given object.
|
|
181
489
|
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
490
|
+
# If you pass an optional block, a {Ractor::Wrapper::Configuration} object
|
|
491
|
+
# will be yielded to it, allowing additional configuration before the wrapper
|
|
492
|
+
# starts. In particular, per-method configuration must be set in this block.
|
|
493
|
+
# Block-provided settings override keyword arguments.
|
|
184
494
|
#
|
|
185
|
-
#
|
|
495
|
+
# See {Configuration} for more information about the method communication
|
|
496
|
+
# and block settings.
|
|
186
497
|
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
498
|
+
# @param object [Object] The non-shareable object to wrap.
|
|
499
|
+
# @param use_current_ractor [boolean] If true, the wrapper is run in a
|
|
500
|
+
# thread in the current Ractor instead of spawning a new Ractor (the
|
|
501
|
+
# default behavior). This option can be used if the wrapped object
|
|
502
|
+
# cannot be moved or must run in the main Ractor. Can also be set via
|
|
503
|
+
# the configuration block.
|
|
504
|
+
# @param name [String] A name for this wrapper. Used during logging. Can
|
|
505
|
+
# also be set via the configuration block. Defaults to the object_id.
|
|
506
|
+
# @param threads [Integer] The number of worker threads to run.
|
|
507
|
+
# Defaults to 0, which causes the wrapper to run sequentially without
|
|
508
|
+
# spawning workers. Can also be set via the configuration block.
|
|
509
|
+
# @param arguments [:move,:copy] How to communicate method arguments by
|
|
510
|
+
# default. If not specified, defaults to `:copy`.
|
|
511
|
+
# @param results [:move,:copy,:void] How to communicate method return
|
|
512
|
+
# values by default. If not specified, defaults to `:copy`.
|
|
513
|
+
# @param block_arguments [:move,:copy] How to communicate block arguments
|
|
514
|
+
# by default. If not specified, defaults to `:copy`.
|
|
515
|
+
# @param block_results [:move,:copy,:void] How to communicate block result
|
|
516
|
+
# values by default. If not specified, defaults to `:copy`.
|
|
517
|
+
# @param block_environment [:caller,:wrapped] How to execute blocks, and
|
|
518
|
+
# what scope blocks have access to. If not specified, defaults to
|
|
519
|
+
# `:caller`.
|
|
520
|
+
# @param enable_logging [boolean] Set to true to enable logging. Default
|
|
521
|
+
# is false. Can also be set via the configuration block.
|
|
522
|
+
# @yield [config] An optional configuration block.
|
|
523
|
+
# @yieldparam config [Ractor::Wrapper::Configuration]
|
|
524
|
+
#
|
|
525
|
+
def initialize(object,
|
|
526
|
+
use_current_ractor: false,
|
|
527
|
+
name: nil,
|
|
528
|
+
threads: 0,
|
|
529
|
+
arguments: nil,
|
|
530
|
+
results: nil,
|
|
531
|
+
block_arguments: nil,
|
|
532
|
+
block_results: nil,
|
|
533
|
+
block_environment: nil,
|
|
534
|
+
enable_logging: false)
|
|
535
|
+
raise ::Ractor::MovedError, "cannot wrap a moved object" if ::Ractor::MovedObject === object
|
|
190
536
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
MethodSettings.new(move: move, move_arguments: move_arguments, move_return: move_return)
|
|
537
|
+
config = Configuration.new
|
|
538
|
+
config.name = name || object_id.to_s
|
|
539
|
+
config.enable_logging = enable_logging
|
|
540
|
+
config.threads = threads
|
|
541
|
+
config.use_current_ractor = use_current_ractor
|
|
542
|
+
config.configure_method(arguments: arguments,
|
|
543
|
+
results: results,
|
|
544
|
+
block_arguments: block_arguments,
|
|
545
|
+
block_results: block_results,
|
|
546
|
+
block_environment: block_environment)
|
|
547
|
+
yield config if block_given?
|
|
548
|
+
|
|
549
|
+
@name = config.name
|
|
550
|
+
@enable_logging = config.enable_logging
|
|
551
|
+
@threads = config.threads
|
|
552
|
+
@method_settings = config.final_method_settings
|
|
553
|
+
@stub = Stub.new(self)
|
|
554
|
+
|
|
555
|
+
if config.use_current_ractor
|
|
556
|
+
setup_local_server(object)
|
|
557
|
+
else
|
|
558
|
+
setup_isolated_server(object)
|
|
559
|
+
end
|
|
215
560
|
end
|
|
216
561
|
|
|
217
562
|
##
|
|
218
|
-
# Return the
|
|
219
|
-
# methods as the wrapped object, providing an easy way to call a wrapper.
|
|
563
|
+
# Return the name of this wrapper.
|
|
220
564
|
#
|
|
221
|
-
# @return [
|
|
565
|
+
# @return [String]
|
|
222
566
|
#
|
|
223
|
-
attr_reader :
|
|
567
|
+
attr_reader :name
|
|
224
568
|
|
|
225
569
|
##
|
|
226
|
-
#
|
|
570
|
+
# Determine whether this wrapper runs in the current Ractor
|
|
227
571
|
#
|
|
228
|
-
# @return [
|
|
572
|
+
# @return [boolean]
|
|
229
573
|
#
|
|
230
|
-
|
|
574
|
+
def use_current_ractor?
|
|
575
|
+
@ractor.nil?
|
|
576
|
+
end
|
|
231
577
|
|
|
232
578
|
##
|
|
233
579
|
# Return whether logging is enabled for this wrapper.
|
|
234
580
|
#
|
|
235
581
|
# @return [Boolean]
|
|
236
582
|
#
|
|
237
|
-
|
|
583
|
+
def enable_logging?
|
|
584
|
+
@enable_logging
|
|
585
|
+
end
|
|
238
586
|
|
|
239
587
|
##
|
|
240
|
-
# Return the
|
|
588
|
+
# Return the number of worker threads used by the wrapper.
|
|
241
589
|
#
|
|
242
|
-
# @return [
|
|
590
|
+
# @return [Integer]
|
|
243
591
|
#
|
|
244
|
-
attr_reader :
|
|
592
|
+
attr_reader :threads
|
|
245
593
|
|
|
246
594
|
##
|
|
247
595
|
# Return the method settings for the given method name. This returns the
|
|
@@ -253,10 +601,17 @@ class Ractor
|
|
|
253
601
|
# @return [MethodSettings]
|
|
254
602
|
#
|
|
255
603
|
def method_settings(method_name)
|
|
256
|
-
method_name
|
|
257
|
-
@method_settings[method_name] || @method_settings[nil]
|
|
604
|
+
(method_name && @method_settings[method_name.to_sym]) || @method_settings[nil]
|
|
258
605
|
end
|
|
259
606
|
|
|
607
|
+
##
|
|
608
|
+
# Return the wrapper stub. This is an object that responds to the same
|
|
609
|
+
# methods as the wrapped object, providing an easy way to call a wrapper.
|
|
610
|
+
#
|
|
611
|
+
# @return [Ractor::Wrapper::Stub]
|
|
612
|
+
#
|
|
613
|
+
attr_reader :stub
|
|
614
|
+
|
|
260
615
|
##
|
|
261
616
|
# A lower-level interface for calling methods through the wrapper.
|
|
262
617
|
#
|
|
@@ -265,407 +620,681 @@ class Ractor
|
|
|
265
620
|
# @param kwargs [keywords] The keyword arguments
|
|
266
621
|
# @return [Object] The return value
|
|
267
622
|
#
|
|
268
|
-
def call(method_name, *args, **kwargs)
|
|
269
|
-
|
|
270
|
-
transaction =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
623
|
+
def call(method_name, *args, **kwargs, &)
|
|
624
|
+
reply_port = ::Ractor::Port.new
|
|
625
|
+
transaction = make_transaction
|
|
626
|
+
settings = method_settings(method_name)
|
|
627
|
+
block_arg = make_block_arg(settings, &)
|
|
628
|
+
message = CallMessage.new(method_name: method_name,
|
|
629
|
+
args: args,
|
|
630
|
+
kwargs: kwargs,
|
|
631
|
+
block_arg: block_arg,
|
|
632
|
+
transaction: transaction,
|
|
633
|
+
settings: settings,
|
|
634
|
+
reply_port: reply_port)
|
|
635
|
+
maybe_log("Sending method", method_name: method_name, transaction: transaction)
|
|
636
|
+
begin
|
|
637
|
+
@port.send(message, move: settings.arguments == :move)
|
|
638
|
+
rescue ::Ractor::ClosedError
|
|
639
|
+
raise StoppedError, "Wrapper has stopped"
|
|
640
|
+
end
|
|
641
|
+
loop do
|
|
642
|
+
reply_message = reply_port.receive
|
|
643
|
+
case reply_message
|
|
644
|
+
when YieldMessage
|
|
645
|
+
handle_yield(reply_message, transaction, settings, method_name, &)
|
|
646
|
+
when ReturnMessage
|
|
647
|
+
maybe_log("Received result", method_name: method_name, transaction: transaction)
|
|
648
|
+
return reply_message.value
|
|
649
|
+
when ExceptionMessage
|
|
650
|
+
maybe_log("Received exception", method_name: method_name, transaction: transaction)
|
|
651
|
+
raise reply_message.exception
|
|
652
|
+
end
|
|
282
653
|
end
|
|
654
|
+
ensure
|
|
655
|
+
reply_port.close
|
|
283
656
|
end
|
|
284
657
|
|
|
285
658
|
##
|
|
286
659
|
# Request that the wrapper stop. All currently running calls will complete
|
|
287
660
|
# before the wrapper actually terminates. However, any new calls will fail.
|
|
288
661
|
#
|
|
289
|
-
# This
|
|
662
|
+
# This method is idempotent and can be called multiple times (even from
|
|
290
663
|
# different ractors).
|
|
291
664
|
#
|
|
292
665
|
# @return [self]
|
|
293
666
|
#
|
|
294
667
|
def async_stop
|
|
295
|
-
maybe_log("Stopping
|
|
296
|
-
@
|
|
668
|
+
maybe_log("Stopping wrapper")
|
|
669
|
+
@port.send(StopMessage.new.freeze)
|
|
297
670
|
self
|
|
298
671
|
rescue ::Ractor::ClosedError
|
|
299
672
|
# Ignore to allow stops to be idempotent.
|
|
300
673
|
self
|
|
301
674
|
end
|
|
302
675
|
|
|
676
|
+
##
|
|
677
|
+
# Blocks until the wrapper has fully stopped.
|
|
678
|
+
#
|
|
679
|
+
# Unlike `Thread#join` and `Ractor#join`, if a Wrapper crashes, the
|
|
680
|
+
# exception generally does *not* get raised out of `Wrapper#join`. Instead,
|
|
681
|
+
# it just returns self in the same way as normal termination.
|
|
682
|
+
#
|
|
683
|
+
# @return [self]
|
|
684
|
+
#
|
|
685
|
+
def join
|
|
686
|
+
if @ractor
|
|
687
|
+
@ractor.join
|
|
688
|
+
else
|
|
689
|
+
reply_port = ::Ractor::Port.new
|
|
690
|
+
begin
|
|
691
|
+
@port.send(JoinMessage.new(reply_port))
|
|
692
|
+
reply_port.receive
|
|
693
|
+
rescue ::Ractor::ClosedError
|
|
694
|
+
# Assume the wrapper has stopped if the port is not sendable
|
|
695
|
+
ensure
|
|
696
|
+
reply_port.close
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
self
|
|
700
|
+
end
|
|
701
|
+
|
|
303
702
|
##
|
|
304
703
|
# Retrieves the original object that was wrapped. This should be called
|
|
305
704
|
# only after a stop request has been issued using {#async_stop}, and may
|
|
306
705
|
# block until the wrapper has fully stopped.
|
|
307
706
|
#
|
|
308
|
-
#
|
|
707
|
+
# This can be called only if the wrapper was *not* configured with
|
|
708
|
+
# `use_current_ractor: true`. If the wrapper had that configuration, the
|
|
709
|
+
# object will not be moved, and does not need to be recovered. In such a
|
|
710
|
+
# case, any calls to this method will raise Ractor::Error.
|
|
711
|
+
#
|
|
712
|
+
# Only one ractor may call this method; any additional calls will fail with
|
|
713
|
+
# a Ractor::Wrapper::Error.
|
|
309
714
|
#
|
|
310
715
|
# @return [Object] The original wrapped object
|
|
311
716
|
#
|
|
312
|
-
def
|
|
313
|
-
@ractor
|
|
717
|
+
def recover_object
|
|
718
|
+
raise Error, "cannot recover an object from a local wrapper" unless @ractor
|
|
719
|
+
begin
|
|
720
|
+
@ractor.value
|
|
721
|
+
rescue ::Ractor::Error => e
|
|
722
|
+
raise ::Ractor::Wrapper::Error, e.message, cause: e
|
|
723
|
+
end
|
|
314
724
|
end
|
|
315
725
|
|
|
316
|
-
private
|
|
726
|
+
#### private items below ####
|
|
317
727
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
end
|
|
728
|
+
##
|
|
729
|
+
# @private
|
|
730
|
+
# Message sent to initialize a server.
|
|
731
|
+
#
|
|
732
|
+
InitMessage = ::Data.define(:object, :stub)
|
|
324
733
|
|
|
325
734
|
##
|
|
326
|
-
#
|
|
735
|
+
# @private
|
|
736
|
+
# Message sent to a server to call a method
|
|
327
737
|
#
|
|
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
|
|
738
|
+
CallMessage = ::Data.define(:method_name, :args, :kwargs, :block_arg,
|
|
739
|
+
:transaction, :settings, :reply_port)
|
|
338
740
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
@wrapper.call(name, *args, **kwargs)
|
|
345
|
-
end
|
|
741
|
+
##
|
|
742
|
+
# @private
|
|
743
|
+
# Message sent to a server when a worker thread terminates
|
|
744
|
+
#
|
|
745
|
+
WorkerStoppedMessage = ::Data.define(:worker_num)
|
|
346
746
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
@wrapper.call(:respond_to?, name, include_all)
|
|
353
|
-
end
|
|
354
|
-
end
|
|
747
|
+
##
|
|
748
|
+
# @private
|
|
749
|
+
# Message sent to a server to request it to stop
|
|
750
|
+
#
|
|
751
|
+
StopMessage = ::Data.define
|
|
355
752
|
|
|
356
753
|
##
|
|
357
|
-
#
|
|
358
|
-
#
|
|
754
|
+
# @private
|
|
755
|
+
# Message sent to a server to request a join response
|
|
359
756
|
#
|
|
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
|
|
757
|
+
JoinMessage = ::Data.define(:reply_port)
|
|
369
758
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
end
|
|
759
|
+
##
|
|
760
|
+
# @private
|
|
761
|
+
# Message sent from a server in response to a join request.
|
|
762
|
+
#
|
|
763
|
+
JoinReplyMessage = ::Data.define
|
|
376
764
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
end
|
|
765
|
+
##
|
|
766
|
+
# @private
|
|
767
|
+
# Message sent to report a return value
|
|
768
|
+
#
|
|
769
|
+
ReturnMessage = ::Data.define(:value)
|
|
383
770
|
|
|
384
|
-
|
|
771
|
+
##
|
|
772
|
+
# @private
|
|
773
|
+
# Message sent to report an exception result
|
|
774
|
+
#
|
|
775
|
+
ExceptionMessage = ::Data.define(:exception)
|
|
385
776
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
777
|
+
##
|
|
778
|
+
# @private
|
|
779
|
+
# Message sent from a server to request a yield block run
|
|
780
|
+
#
|
|
781
|
+
YieldMessage = ::Data.define(:args, :kwargs, :reply_port)
|
|
782
|
+
|
|
783
|
+
private
|
|
784
|
+
|
|
785
|
+
##
|
|
786
|
+
# Start a server in the current Ractor.
|
|
787
|
+
# Passes the object directly to the server.
|
|
788
|
+
#
|
|
789
|
+
def setup_local_server(object)
|
|
790
|
+
maybe_log("Starting local server")
|
|
791
|
+
@ractor = nil
|
|
792
|
+
@port = ::Ractor::Port.new
|
|
793
|
+
freeze
|
|
794
|
+
wrapper_id = object_id
|
|
795
|
+
::Thread.new do
|
|
796
|
+
::Thread.current.name = "ractor-wrapper:server:#{wrapper_id}"
|
|
797
|
+
Server.run_local(object: object,
|
|
798
|
+
stub: @stub,
|
|
799
|
+
port: @port,
|
|
800
|
+
name: name,
|
|
801
|
+
enable_logging: enable_logging?,
|
|
802
|
+
threads: threads)
|
|
392
803
|
end
|
|
393
804
|
end
|
|
394
805
|
|
|
395
806
|
##
|
|
396
|
-
#
|
|
397
|
-
# This
|
|
398
|
-
#
|
|
807
|
+
# Start a server in an isolated Ractor.
|
|
808
|
+
# This must send the object separately since it must be moved into the
|
|
809
|
+
# server's Ractor.
|
|
399
810
|
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
# Ractor's functionality.)
|
|
407
|
-
#
|
|
408
|
-
class Message
|
|
409
|
-
# @private
|
|
410
|
-
def initialize(type, data: nil, transaction: nil)
|
|
411
|
-
@sender = ::Ractor.current
|
|
412
|
-
@type = type
|
|
413
|
-
@data = data
|
|
414
|
-
@transaction = transaction || new_transaction
|
|
415
|
-
freeze
|
|
811
|
+
def setup_isolated_server(object)
|
|
812
|
+
maybe_log("Starting isolated server")
|
|
813
|
+
@ractor = ::Ractor.new(name, enable_logging?, threads, name: "wrapper:#{name}") do |name, enable_logging, threads|
|
|
814
|
+
Server.run_isolated(name: name,
|
|
815
|
+
enable_logging: enable_logging,
|
|
816
|
+
threads: threads)
|
|
416
817
|
end
|
|
818
|
+
@port = @ractor.default_port
|
|
819
|
+
freeze
|
|
820
|
+
init_message = InitMessage.new(object: object, stub: @stub)
|
|
821
|
+
@port.send(init_message, move: true)
|
|
822
|
+
end
|
|
417
823
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
# @private
|
|
425
|
-
attr_reader :transaction
|
|
426
|
-
|
|
427
|
-
# @private
|
|
428
|
-
attr_reader :data
|
|
429
|
-
|
|
430
|
-
private
|
|
824
|
+
##
|
|
825
|
+
# Create a transaction ID, used for logging
|
|
826
|
+
#
|
|
827
|
+
def make_transaction
|
|
828
|
+
::Random.rand(7_958_661_109_946_400_884_391_936).to_s(36).rjust(16, "0").freeze
|
|
829
|
+
end
|
|
431
830
|
|
|
432
|
-
|
|
433
|
-
|
|
831
|
+
##
|
|
832
|
+
# Create the shareable object representing a block in a method call
|
|
833
|
+
#
|
|
834
|
+
def make_block_arg(settings, &)
|
|
835
|
+
if !block_given?
|
|
836
|
+
nil
|
|
837
|
+
elsif settings.block_environment == :wrapped
|
|
838
|
+
::Ractor.shareable_proc(&)
|
|
839
|
+
else
|
|
840
|
+
:send_block_message
|
|
434
841
|
end
|
|
435
842
|
end
|
|
436
843
|
|
|
437
844
|
##
|
|
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.
|
|
845
|
+
# Handle a call to a block directed to run in the caller environment.
|
|
442
846
|
#
|
|
443
|
-
|
|
444
|
-
|
|
847
|
+
def handle_yield(message, transaction, settings, method_name)
|
|
848
|
+
maybe_log("Yielding to block", method_name: method_name, transaction: transaction)
|
|
849
|
+
begin
|
|
850
|
+
block_result = yield(*message.args, **message.kwargs)
|
|
851
|
+
block_result = nil if settings.block_results == :void
|
|
852
|
+
maybe_log("Sending block result", method_name: method_name, transaction: transaction)
|
|
853
|
+
message.reply_port.send(ReturnMessage.new(block_result), move: settings.block_results == :move)
|
|
854
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
855
|
+
maybe_log("Sending block exception", method_name: method_name, transaction: transaction)
|
|
856
|
+
begin
|
|
857
|
+
message.reply_port.send(ExceptionMessage.new(e))
|
|
858
|
+
rescue ::StandardError
|
|
859
|
+
begin
|
|
860
|
+
message.reply_port.send(ExceptionMessage.new(::StandardError.new(e.inspect)))
|
|
861
|
+
rescue ::StandardError
|
|
862
|
+
maybe_log("Failure to send block reply", method_name: method_name, transaction: transaction)
|
|
863
|
+
end
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
##
|
|
869
|
+
# Prints out a log message
|
|
445
870
|
#
|
|
871
|
+
def maybe_log(str, transaction: nil, method_name: nil)
|
|
872
|
+
return unless enable_logging?
|
|
873
|
+
metadata = [::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%L"), "Ractor::Wrapper/#{name}"]
|
|
874
|
+
metadata << "Transaction/#{transaction}" if transaction
|
|
875
|
+
metadata << "Method/#{method_name}" if method_name
|
|
876
|
+
metadata = metadata.join(" ")
|
|
877
|
+
$stderr.puts("[#{metadata}] #{str}")
|
|
878
|
+
$stderr.flush
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
##
|
|
446
882
|
# @private
|
|
447
883
|
#
|
|
884
|
+
# Server is the backend implementation of a wrapper. It listens for method
|
|
885
|
+
# call requests on a port, and calls the wrapped object in a controlled
|
|
886
|
+
# environment.
|
|
887
|
+
#
|
|
888
|
+
# It can run:
|
|
889
|
+
#
|
|
890
|
+
# * Either hosted by an external Ractor or isolated in a dedicated Ractor
|
|
891
|
+
# * Either sequentially or concurrently using worker threads.
|
|
892
|
+
#
|
|
448
893
|
class Server
|
|
449
894
|
##
|
|
450
|
-
#
|
|
895
|
+
# @private
|
|
896
|
+
# Create and run a server hosted in the current Ractor
|
|
451
897
|
#
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
898
|
+
def self.run_local(object:, stub:, port:, name:, enable_logging: false, threads: 0)
|
|
899
|
+
server = new(isolated: false, object:, stub:, port:, name:, enable_logging:, threads:)
|
|
900
|
+
server.run
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
##
|
|
904
|
+
# @private
|
|
905
|
+
# Create and run a server in an isolated Ractor
|
|
456
906
|
#
|
|
457
|
-
|
|
458
|
-
|
|
907
|
+
def self.run_isolated(name:, enable_logging: false, threads: 0)
|
|
908
|
+
port = ::Ractor.current.default_port
|
|
909
|
+
server = new(isolated: true, object: nil, stub: nil, port:, name:, enable_logging:, threads:)
|
|
910
|
+
server.run
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# @private
|
|
914
|
+
def initialize(isolated:, object:, stub:, port:, name:, enable_logging:, threads:)
|
|
915
|
+
@isolated = isolated
|
|
916
|
+
@object = object
|
|
917
|
+
@stub = stub
|
|
918
|
+
@port = port
|
|
919
|
+
@name = name
|
|
920
|
+
@enable_logging = enable_logging
|
|
921
|
+
@threads_requested = threads.positive? ? threads : false
|
|
922
|
+
@join_requests = []
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
##
|
|
926
|
+
# @private
|
|
927
|
+
# Handle the server lifecycle.
|
|
928
|
+
# Returns the wrapped object, so it can be recovered if the server is run
|
|
929
|
+
# in a Ractor.
|
|
459
930
|
#
|
|
460
931
|
def run
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
932
|
+
receive_remote_object if @isolated
|
|
933
|
+
start_workers if @threads_requested
|
|
934
|
+
main_loop
|
|
935
|
+
stop_workers if @threads_requested
|
|
936
|
+
cleanup
|
|
465
937
|
@object
|
|
466
|
-
rescue ::
|
|
467
|
-
|
|
938
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
939
|
+
@crash_exception = e
|
|
468
940
|
@object
|
|
941
|
+
ensure
|
|
942
|
+
crash_cleanup if @crash_exception
|
|
469
943
|
end
|
|
470
944
|
|
|
471
945
|
private
|
|
472
946
|
|
|
473
947
|
##
|
|
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.
|
|
948
|
+
# Receive the moved remote object. Called if the server is run in a
|
|
949
|
+
# separate Ractor.
|
|
481
950
|
#
|
|
482
|
-
def
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
@
|
|
486
|
-
@
|
|
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")
|
|
951
|
+
def receive_remote_object
|
|
952
|
+
maybe_log("Waiting for initialization")
|
|
953
|
+
init_message = @port.receive
|
|
954
|
+
@object = init_message.object
|
|
955
|
+
@stub = init_message.stub
|
|
497
956
|
end
|
|
498
957
|
|
|
499
958
|
##
|
|
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.
|
|
959
|
+
# Start the worker threads. Each thread picks up methods to run from a
|
|
960
|
+
# shared queue. Called only if worker threading is enabled.
|
|
506
961
|
#
|
|
507
|
-
def
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
handle_method(worker_num, request)
|
|
514
|
-
unregister_call(request.transaction)
|
|
962
|
+
def start_workers
|
|
963
|
+
maybe_log("Spawning #{@threads_requested} worker threads")
|
|
964
|
+
@queue = ::Queue.new
|
|
965
|
+
@active_workers = {}
|
|
966
|
+
(1..@threads_requested).each do |worker_num|
|
|
967
|
+
@active_workers[worker_num] = ::Thread.new { worker_thread(worker_num) }
|
|
515
968
|
end
|
|
516
|
-
ensure
|
|
517
|
-
maybe_worker_log(worker_num, "Stopping")
|
|
518
|
-
::Ractor.current.send(Message.new(:thread_stopped, data: worker_num), move: true)
|
|
519
969
|
end
|
|
520
970
|
|
|
521
971
|
##
|
|
522
|
-
#
|
|
523
|
-
#
|
|
972
|
+
# This is the main loop, listening on the inbox and handling messages for
|
|
973
|
+
# normal operation:
|
|
524
974
|
#
|
|
525
|
-
# * If it receives a
|
|
526
|
-
#
|
|
527
|
-
#
|
|
528
|
-
#
|
|
529
|
-
# * If it receives a
|
|
530
|
-
#
|
|
531
|
-
#
|
|
532
|
-
#
|
|
533
|
-
# stopping
|
|
975
|
+
# * If it receives a CallMessage, it either runs the method (when in
|
|
976
|
+
# sequential mode) or adds it to the job queue (when in worker mode).
|
|
977
|
+
# * If it receives a StopMessage, it exits the main loop and proceeds
|
|
978
|
+
# to the termination logic.
|
|
979
|
+
# * If it receives a JoinMessage, it adds it to the list of join ports
|
|
980
|
+
# to notify once the wrapper completes.
|
|
981
|
+
# * If it receives a WorkerStoppedMessage, that indicates a worker
|
|
982
|
+
# thread has unexpectedly stopped. We conclude something has gone
|
|
983
|
+
# wrong with a worker, and we bail, stopping the remaining workers
|
|
984
|
+
# and proceeding to termination logic.
|
|
534
985
|
#
|
|
535
|
-
def
|
|
986
|
+
def main_loop
|
|
536
987
|
loop do
|
|
537
|
-
maybe_log("Waiting for message")
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
@
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
988
|
+
maybe_log("Waiting for message in running phase")
|
|
989
|
+
message = @port.receive
|
|
990
|
+
case message
|
|
991
|
+
when CallMessage
|
|
992
|
+
maybe_log("Received CallMessage", call_message: message)
|
|
993
|
+
if @threads_requested
|
|
994
|
+
@queue.enq(message)
|
|
995
|
+
else
|
|
996
|
+
handle_method(message)
|
|
997
|
+
end
|
|
998
|
+
when WorkerStoppedMessage
|
|
999
|
+
maybe_log("Received unexpected WorkerStoppedMessage")
|
|
1000
|
+
@active_workers.delete(message.worker_num) if @threads_requested
|
|
548
1001
|
break
|
|
549
|
-
when
|
|
1002
|
+
when StopMessage
|
|
550
1003
|
maybe_log("Received stop")
|
|
551
1004
|
break
|
|
1005
|
+
when JoinMessage
|
|
1006
|
+
maybe_log("Received and queueing join request")
|
|
1007
|
+
@join_requests << message.reply_port
|
|
552
1008
|
end
|
|
553
1009
|
end
|
|
554
1010
|
end
|
|
555
1011
|
|
|
556
1012
|
##
|
|
557
|
-
#
|
|
558
|
-
#
|
|
559
|
-
#
|
|
560
|
-
#
|
|
561
|
-
#
|
|
562
|
-
#
|
|
1013
|
+
# This signals workers to stop by closing the queue, and then waits for
|
|
1014
|
+
# all workers to report in that they have stopped. It is called only if
|
|
1015
|
+
# worker threading is enabled.
|
|
1016
|
+
#
|
|
1017
|
+
# Responds to messages to indicate the wrapper is stopping and no longer
|
|
1018
|
+
# accepting new method requests:
|
|
1019
|
+
#
|
|
1020
|
+
# * If it receives a CallMessage, it sends back a refusal exception.
|
|
1021
|
+
# * If it receives a StopMessage, it does nothing (i.e. the stop
|
|
1022
|
+
# operation is idempotent).
|
|
1023
|
+
# * If it receives a JoinMessage, it adds it to the list of join ports
|
|
1024
|
+
# to notify once the wrapper completes. At this point the wrapper is
|
|
1025
|
+
# not yet considered complete because workers are still processing
|
|
1026
|
+
# earlier method calls.
|
|
1027
|
+
# * If it receives a WorkerStoppedMessage, it updates its count of
|
|
1028
|
+
# running workers.
|
|
563
1029
|
#
|
|
564
|
-
|
|
1030
|
+
# This phase continues until all workers have signaled that they have
|
|
1031
|
+
# stopped.
|
|
1032
|
+
#
|
|
1033
|
+
def stop_workers
|
|
565
1034
|
@queue.close
|
|
566
|
-
|
|
567
|
-
maybe_log("Waiting for message
|
|
568
|
-
message =
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
when :call
|
|
1035
|
+
until @active_workers.empty?
|
|
1036
|
+
maybe_log("Waiting for message in stopping phase")
|
|
1037
|
+
message = @port.receive
|
|
1038
|
+
case message
|
|
1039
|
+
when CallMessage
|
|
572
1040
|
refuse_method(message)
|
|
573
|
-
when
|
|
574
|
-
|
|
1041
|
+
when WorkerStoppedMessage
|
|
1042
|
+
maybe_log("Acknowledged WorkerStoppedMessage: #{message.worker_num}")
|
|
1043
|
+
@active_workers.delete(message.worker_num)
|
|
1044
|
+
when StopMessage
|
|
1045
|
+
maybe_log("Stop received when already stopping")
|
|
1046
|
+
when JoinMessage
|
|
1047
|
+
maybe_log("Received and queueing join request")
|
|
1048
|
+
@join_requests << message.reply_port
|
|
575
1049
|
end
|
|
576
1050
|
end
|
|
577
1051
|
end
|
|
578
1052
|
|
|
579
1053
|
##
|
|
580
|
-
#
|
|
581
|
-
#
|
|
582
|
-
# requests with a refusal. It also makes another pass through the pending
|
|
583
|
-
# requests; if there are any left, it probably means a worker thread died
|
|
584
|
-
# without responding to it preoprly, so we send back an error message.
|
|
1054
|
+
# This is called when the Server is ready to terminate completely.
|
|
1055
|
+
# It closes the inbox and responds to any remaining contents.
|
|
585
1056
|
#
|
|
586
|
-
def
|
|
587
|
-
|
|
588
|
-
|
|
1057
|
+
def cleanup
|
|
1058
|
+
maybe_log("Closing inbox")
|
|
1059
|
+
@port.close
|
|
1060
|
+
maybe_log("Draining inbox")
|
|
589
1061
|
loop do
|
|
590
|
-
message =
|
|
591
|
-
|
|
1062
|
+
message = begin
|
|
1063
|
+
@port.receive
|
|
1064
|
+
rescue ::Ractor::ClosedError
|
|
1065
|
+
maybe_log("Inbox is empty")
|
|
1066
|
+
nil
|
|
1067
|
+
end
|
|
1068
|
+
break if message.nil?
|
|
1069
|
+
case message
|
|
1070
|
+
when CallMessage
|
|
1071
|
+
refuse_method(message)
|
|
1072
|
+
when WorkerStoppedMessage
|
|
1073
|
+
maybe_log("Unexpected WorkerStoppedMessage when in cleanup")
|
|
1074
|
+
when StopMessage
|
|
1075
|
+
maybe_log("Stop received when already stopping")
|
|
1076
|
+
when JoinMessage
|
|
1077
|
+
maybe_log("Received and responding immediately to join request")
|
|
1078
|
+
send_join_reply(message.reply_port)
|
|
1079
|
+
end
|
|
592
1080
|
end
|
|
593
|
-
maybe_log("
|
|
594
|
-
@
|
|
595
|
-
|
|
1081
|
+
maybe_log("Responding to join requests")
|
|
1082
|
+
@join_requests.each { |port| send_join_reply(port) }
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
##
|
|
1086
|
+
# Called from the ensure block in run when an unexpected exception
|
|
1087
|
+
# terminated the server. Drains pending requests that are not otherwise
|
|
1088
|
+
# being handled, responding to all pending callers and join requesters,
|
|
1089
|
+
# and also joins any worker threads.
|
|
1090
|
+
#
|
|
1091
|
+
def crash_cleanup
|
|
1092
|
+
maybe_log("Running crash cleanup after: #{@crash_exception.message} (#{@crash_exception.class})")
|
|
1093
|
+
error = CrashedError.new("Server crashed: #{@crash_exception.message} (#{@crash_exception.class})")
|
|
1094
|
+
# `@queue` should not be nil in threaded mode, but we're checking
|
|
1095
|
+
# anyway just in case a crash happened during setup
|
|
1096
|
+
drain_queue_after_crash(@queue, error) if @threads_requested && @queue
|
|
1097
|
+
drain_inbox_after_crash(@port, error)
|
|
1098
|
+
# `@active_workers` should not be nil in threaded mode, but we're
|
|
1099
|
+
# checking anyway just in case a crash happened during setup
|
|
1100
|
+
join_workers_after_crash(@active_workers) if @threads_requested && @active_workers
|
|
1101
|
+
@join_requests.each { |port| send_join_reply(port) }
|
|
1102
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
1103
|
+
maybe_log("Suppressed exception during crash_cleanup: #{e.message} (#{e.class})")
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
##
|
|
1107
|
+
# Drains any remaining queued call messages after a crash, sending errors
|
|
1108
|
+
# to callers whose calls had not yet been dispatched to a worker thread.
|
|
1109
|
+
#
|
|
1110
|
+
def drain_queue_after_crash(queue, error)
|
|
1111
|
+
queue.close
|
|
1112
|
+
loop do
|
|
1113
|
+
message = queue.deq
|
|
1114
|
+
break if message.nil?
|
|
1115
|
+
begin
|
|
1116
|
+
message.reply_port.send(ExceptionMessage.new(error))
|
|
1117
|
+
rescue ::Ractor::Error
|
|
1118
|
+
maybe_log("Failed to send crash error to queued caller", call_message: message)
|
|
1119
|
+
end
|
|
596
1120
|
end
|
|
597
|
-
rescue ::
|
|
598
|
-
maybe_log("
|
|
1121
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
1122
|
+
maybe_log("Suppressed exception during drain_queue_after_crash: " \
|
|
1123
|
+
"#{e.message} (#{e.class})")
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
##
|
|
1127
|
+
# Drains any remaining inbox messages after a crash, sending errors to
|
|
1128
|
+
# pending callers and responding to any join requests.
|
|
1129
|
+
#
|
|
1130
|
+
def drain_inbox_after_crash(port, error)
|
|
1131
|
+
begin
|
|
1132
|
+
port.close
|
|
1133
|
+
rescue ::Ractor::Error
|
|
1134
|
+
# Port was already closed (maybe because it was the cause of the crash)
|
|
1135
|
+
end
|
|
1136
|
+
loop do
|
|
1137
|
+
message = begin
|
|
1138
|
+
port.receive
|
|
1139
|
+
rescue ::Ractor::Error
|
|
1140
|
+
nil
|
|
1141
|
+
end
|
|
1142
|
+
break if message.nil?
|
|
1143
|
+
case message
|
|
1144
|
+
when CallMessage
|
|
1145
|
+
begin
|
|
1146
|
+
message.reply_port.send(ExceptionMessage.new(error))
|
|
1147
|
+
rescue ::Ractor::Error
|
|
1148
|
+
maybe_log("Failed to send crash error to caller", call_message: message)
|
|
1149
|
+
end
|
|
1150
|
+
when JoinMessage
|
|
1151
|
+
send_join_reply(message.reply_port)
|
|
1152
|
+
when WorkerStoppedMessage, StopMessage
|
|
1153
|
+
# Ignore
|
|
1154
|
+
end
|
|
1155
|
+
end
|
|
1156
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
1157
|
+
maybe_log("Suppressed exception during drain_inbox_after_crash: #{e.message} (#{e.class})")
|
|
599
1158
|
end
|
|
600
1159
|
|
|
601
1160
|
##
|
|
602
|
-
#
|
|
1161
|
+
# Wait until all workers have stopped after a crash
|
|
1162
|
+
#
|
|
1163
|
+
def join_workers_after_crash(workers)
|
|
1164
|
+
workers.each_value do |thread|
|
|
1165
|
+
thread.join
|
|
1166
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
1167
|
+
maybe_log("Suppressed exception during join_workers_after_crash: #{e.message} (#{e.class})")
|
|
1168
|
+
end
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
##
|
|
1172
|
+
# A worker thread repeatedly pulls a method call requests off the job
|
|
1173
|
+
# queue, handles it, and sends back a response. It also removes the
|
|
1174
|
+
# request from the pending request list to signal that it has responded.
|
|
1175
|
+
# If no job is available, the thread blocks while waiting. If the queue
|
|
1176
|
+
# is closed, the worker will send an acknowledgement message and then
|
|
1177
|
+
# terminate.
|
|
1178
|
+
#
|
|
1179
|
+
def worker_thread(worker_num)
|
|
1180
|
+
maybe_log("Worker starting", worker_num: worker_num)
|
|
1181
|
+
loop do
|
|
1182
|
+
maybe_log("Waiting for job", worker_num: worker_num)
|
|
1183
|
+
message = @queue.deq
|
|
1184
|
+
break if message.nil?
|
|
1185
|
+
handle_method(message, worker_num: worker_num)
|
|
1186
|
+
end
|
|
1187
|
+
ensure
|
|
1188
|
+
maybe_log("Worker stopping", worker_num: worker_num)
|
|
1189
|
+
begin
|
|
1190
|
+
@port.send(WorkerStoppedMessage.new(worker_num))
|
|
1191
|
+
rescue ::Ractor::ClosedError
|
|
1192
|
+
maybe_log("Worker unable to report stop, possibly due to server crash", worker_num: worker_num)
|
|
1193
|
+
end
|
|
1194
|
+
end
|
|
1195
|
+
|
|
1196
|
+
##
|
|
1197
|
+
# This is called to handle a method call request.
|
|
603
1198
|
# It calls the method on the wrapped object, and then sends back a
|
|
604
1199
|
# response to the caller. If an exception was raised, it sends back an
|
|
605
1200
|
# error response. It tries very hard always to send a response of some
|
|
606
1201
|
# kind; if an error occurs while constructing or sending a response, it
|
|
607
|
-
# will catch the exception and try to send a simpler response.
|
|
1202
|
+
# will catch the exception and try to send a simpler response. If a block
|
|
1203
|
+
# was passed to the method, it is also handled here.
|
|
608
1204
|
#
|
|
609
|
-
def handle_method(
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1205
|
+
def handle_method(message, worker_num: nil)
|
|
1206
|
+
block = make_block(message)
|
|
1207
|
+
maybe_log("Running method", worker_num: worker_num, call_message: message)
|
|
1208
|
+
result = @object.__send__(message.method_name, *message.args, **message.kwargs, &block)
|
|
1209
|
+
result = @stub if result.equal?(@object)
|
|
1210
|
+
result = nil if message.settings.results == :void
|
|
1211
|
+
maybe_log("Sending return value", worker_num: worker_num, call_message: message)
|
|
1212
|
+
message.reply_port.send(ReturnMessage.new(result), move: message.settings.results == :move)
|
|
1213
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
1214
|
+
maybe_log("Sending exception", worker_num: worker_num, call_message: message)
|
|
614
1215
|
begin
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
sender.send(Message.new(:result, data: result, transaction: transaction),
|
|
618
|
-
move: (@method_settings[method_name] || @method_settings[nil]).move_return?)
|
|
619
|
-
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
|
620
|
-
maybe_worker_log(worker_num, "Sending exception (transaction=#{transaction})")
|
|
1216
|
+
message.reply_port.send(ExceptionMessage.new(e))
|
|
1217
|
+
rescue ::Exception # rubocop:disable Lint/RescueException
|
|
621
1218
|
begin
|
|
622
|
-
|
|
623
|
-
rescue ::
|
|
624
|
-
|
|
625
|
-
::StandardError.new(e.inspect)
|
|
626
|
-
rescue ::StandardError
|
|
627
|
-
::StandardError.new("Unknown error")
|
|
628
|
-
end
|
|
629
|
-
sender.send(Message.new(:error, data: safe_error, transaction: transaction))
|
|
1219
|
+
message.reply_port.send(ExceptionMessage.new(::RuntimeError.new(e.inspect)))
|
|
1220
|
+
rescue ::Exception # rubocop:disable Lint/RescueException
|
|
1221
|
+
maybe_log("Failure to send method response", worker_num: worker_num, call_message: message)
|
|
630
1222
|
end
|
|
631
1223
|
end
|
|
632
1224
|
end
|
|
633
1225
|
|
|
634
1226
|
##
|
|
635
|
-
#
|
|
636
|
-
# the
|
|
637
|
-
# wrapper is shutting down.
|
|
1227
|
+
# Creates a block appropriate to the block specification received with
|
|
1228
|
+
# the method call message. This could return:
|
|
638
1229
|
#
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
def
|
|
646
|
-
|
|
647
|
-
|
|
1230
|
+
# * nil if there was no block
|
|
1231
|
+
# * the proc itself, if a shareable proc was received
|
|
1232
|
+
# * otherwise a proc that sends a message back to the caller, along
|
|
1233
|
+
# with the block arguments, to run the block in the caller's
|
|
1234
|
+
# environment
|
|
1235
|
+
#
|
|
1236
|
+
def make_block(message)
|
|
1237
|
+
return message.block_arg unless message.block_arg == :send_block_message
|
|
1238
|
+
proc do |*args, **kwargs|
|
|
1239
|
+
reply_port = ::Ractor::Port.new
|
|
1240
|
+
reply_message = begin
|
|
1241
|
+
args.map! { |arg| arg.equal?(@object) ? @stub : arg }
|
|
1242
|
+
kwargs.transform_values! { |arg| arg.equal?(@object) ? @stub : arg }
|
|
1243
|
+
yield_message = YieldMessage.new(args: args, kwargs: kwargs, reply_port: reply_port)
|
|
1244
|
+
message.reply_port.send(yield_message, move: message.settings.block_arguments == :move)
|
|
1245
|
+
reply_port.receive
|
|
1246
|
+
ensure
|
|
1247
|
+
reply_port.close
|
|
1248
|
+
end
|
|
1249
|
+
case reply_message
|
|
1250
|
+
when ExceptionMessage
|
|
1251
|
+
raise reply_message.exception
|
|
1252
|
+
when ReturnMessage
|
|
1253
|
+
reply_message.value
|
|
1254
|
+
end
|
|
648
1255
|
end
|
|
649
1256
|
end
|
|
650
1257
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1258
|
+
##
|
|
1259
|
+
# This is called from the main Ractor thread to report to a caller that
|
|
1260
|
+
# the wrapper cannot handle a requested method call, likely because the
|
|
1261
|
+
# wrapper is shutting down.
|
|
1262
|
+
#
|
|
1263
|
+
def refuse_method(message)
|
|
1264
|
+
maybe_log("Refusing method call", call_message: message)
|
|
1265
|
+
begin
|
|
1266
|
+
error = StoppedError.new("Wrapper is shutting down")
|
|
1267
|
+
message.reply_port.send(ExceptionMessage.new(error))
|
|
1268
|
+
rescue ::Ractor::Error
|
|
1269
|
+
maybe_log("Failed to send refusal message", call_message: message)
|
|
654
1270
|
end
|
|
655
1271
|
end
|
|
656
1272
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1273
|
+
##
|
|
1274
|
+
# This attempts to send a signal that a wrapper join has completed.
|
|
1275
|
+
#
|
|
1276
|
+
def send_join_reply(port)
|
|
1277
|
+
port.send(JoinReplyMessage.new.freeze)
|
|
1278
|
+
rescue ::Ractor::ClosedError
|
|
1279
|
+
maybe_log("Join reply port is closed")
|
|
662
1280
|
end
|
|
663
1281
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1282
|
+
##
|
|
1283
|
+
# Print out a log message
|
|
1284
|
+
#
|
|
1285
|
+
def maybe_log(str, call_message: nil, worker_num: nil, transaction: nil, method_name: nil)
|
|
1286
|
+
return unless @enable_logging
|
|
1287
|
+
transaction ||= call_message&.transaction
|
|
1288
|
+
method_name ||= call_message&.method_name
|
|
1289
|
+
metadata = [::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%L"), "Ractor::Wrapper:#{@name}"]
|
|
1290
|
+
metadata << "Worker:#{worker_num}" if worker_num
|
|
1291
|
+
metadata << "Transaction:#{transaction}" if transaction
|
|
1292
|
+
metadata << "Method:#{method_name}" if method_name
|
|
1293
|
+
metadata = metadata.join(" ")
|
|
1294
|
+
$stderr.puts("[#{metadata}] #{str}")
|
|
668
1295
|
$stderr.flush
|
|
1296
|
+
rescue ::StandardError
|
|
1297
|
+
# Swallow any errors during logging
|
|
669
1298
|
end
|
|
670
1299
|
end
|
|
671
1300
|
end
|