ractor-wrapper 0.1.0 → 0.2.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 +7 -0
- data/README.md +49 -44
- data/lib/ractor/wrapper.rb +308 -79
- data/lib/ractor/wrapper/version.rb +1 -1
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9fec3af2b1b8b9c105260fe2fca50d69e48de205dd2dd791592317ee41286af3
|
4
|
+
data.tar.gz: e7b4487502427ec05f3dc530925e9efecd8d599e75f4dc2b82c7371592986f59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ab154c16e2ed53f65a042bf18b85448cb0115e6b1616db5cce96084767ae1f00c23392ba48560177f34c43d757b59e282c05891702af927f40f72fe989b33e1
|
7
|
+
data.tar.gz: 8e16f5694e46c571deac8d66f56bb23467572bad838038d941955f2edaca76537ef741df79b63da0bc28c31708a31ac3ba699e3a2a661b56552dc6b1050625a0
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# Release History
|
2
2
|
|
3
|
+
### v0.2.0 / 2021-03-08
|
4
|
+
|
5
|
+
* BREAKING CHANGE: The wrapper now copies (instead of moves) arguments and return values by default.
|
6
|
+
* It is now possible to control, per method, whether arguments and return values are copied or moved.
|
7
|
+
* Fixed: The respond_to? method did not work correctly for stubs.
|
8
|
+
* Improved: The wrapper server lifecycle is a bit more robust against worker crashes.
|
9
|
+
|
3
10
|
### v0.1.0 / 2021-03-02
|
4
11
|
|
5
12
|
* Initial release. HIGHLY EXPERIMENTAL.
|
data/README.md
CHANGED
@@ -16,18 +16,19 @@ Require it in your code:
|
|
16
16
|
|
17
17
|
You can then create wrappers for objects. See the example below.
|
18
18
|
|
19
|
-
Ractor::Wrapper requires Ruby 3.0.0 or later.
|
19
|
+
`Ractor::Wrapper` requires Ruby 3.0.0 or later.
|
20
20
|
|
21
|
-
WARNING: This is a highly experimental library, and
|
22
|
-
production use. (As of Ruby 3.0.0, the same can be said of Ractors in
|
21
|
+
WARNING: This is a highly experimental library, and currently _not_ recommended
|
22
|
+
for production use. (As of Ruby 3.0.0, the same can be said of Ractors in
|
23
|
+
general.)
|
23
24
|
|
24
25
|
## About Ractor::Wrapper
|
25
26
|
|
26
27
|
Ractors for the most part cannot access objects concurrently with other
|
27
28
|
Ractors unless the object is _shareable_ (that is, deeply immutable along
|
28
|
-
with a few other restrictions.) If multiple Ractors need to
|
29
|
-
resource that is stateful or otherwise not Ractor-shareable, that
|
30
|
-
must itself be a Ractor.
|
29
|
+
with a few other restrictions.) If multiple Ractors need to interact with a
|
30
|
+
shared resource that is stateful or otherwise not Ractor-shareable, that
|
31
|
+
resource must itself be implemented and accessed as a Ractor.
|
31
32
|
|
32
33
|
`Ractor::Wrapper` makes it possible for such a shared resource to be
|
33
34
|
implemented as an ordinary object and accessed using ordinary method calls. It
|
@@ -50,49 +51,53 @@ The following example shows how to share a single `Faraday::Conection`
|
|
50
51
|
object among multiple Ractors. Because `Faraday::Connection` is not itself
|
51
52
|
thread-safe, this example serializes all calls to it.
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
54
|
+
```ruby
|
55
|
+
require "faraday"
|
56
|
+
require "ractor/wrapper"
|
57
|
+
|
58
|
+
# Create a Faraday connection and a wrapper for it.
|
59
|
+
connection = Faraday.new "http://example.com"
|
60
|
+
wrapper = Ractor::Wrapper.new(connection)
|
61
|
+
|
62
|
+
# At this point, the connection object cannot be accessed directly
|
63
|
+
# because it has been "moved" to the wrapper's internal Ractor.
|
64
|
+
# connection.get("/whoops") # <= raises an error
|
65
|
+
|
66
|
+
# However, any number of Ractors can now access it through the wrapper.
|
67
|
+
# By default, access to the object is serialized; methods will not be
|
68
|
+
# invoked concurrently. (To allow concurrent access, set up threads when
|
69
|
+
# creating the wrapper.)
|
70
|
+
r1 = Ractor.new(wrapper) do |w|
|
71
|
+
10.times do
|
72
|
+
w.stub.get("/hello")
|
73
|
+
end
|
74
|
+
:ok
|
75
|
+
end
|
76
|
+
r2 = Ractor.new(wrapper) do |w|
|
77
|
+
10.times do
|
78
|
+
w.stub.get("/ruby")
|
79
|
+
end
|
80
|
+
:ok
|
81
|
+
end
|
82
|
+
|
83
|
+
# Wait for the two above Ractors to finish.
|
84
|
+
r1.take
|
85
|
+
r2.take
|
86
|
+
|
87
|
+
# After you stop the wrapper, you can retrieve the underlying
|
88
|
+
# connection object and access it directly again.
|
89
|
+
wrapper.async_stop
|
90
|
+
connection = wrapper.recover_object
|
91
|
+
connection.get("/finally")
|
92
|
+
```
|
90
93
|
|
91
94
|
### Features
|
92
95
|
|
93
96
|
* Provides a method interface to an object running in a different Ractor.
|
94
97
|
* Supports arbitrary method arguments and return values.
|
95
98
|
* Supports exceptions thrown by the method.
|
99
|
+
* Can be configured to copy or move arguments, return values, and
|
100
|
+
exceptions, per method.
|
96
101
|
* Can serialize method calls for non-concurrency-safe objects, or run
|
97
102
|
methods concurrently in multiple worker threads for thread-safe objects.
|
98
103
|
* Can gracefully shut down the wrapper and retrieve the original object.
|
@@ -127,7 +132,7 @@ Development is done in GitHub at https://github.com/dazuma/ractor-wrapper.
|
|
127
132
|
|
128
133
|
The library uses [toys](https://dazuma.github.io/toys) for testing and CI. To
|
129
134
|
run the test suite, `gem install toys` and then run `toys ci`. You can also run
|
130
|
-
unit tests, rubocop, and builds independently.
|
135
|
+
unit tests, rubocop, and builds independently.
|
131
136
|
|
132
137
|
## License
|
133
138
|
|
data/lib/ractor/wrapper.rb
CHANGED
@@ -6,13 +6,17 @@ class Ractor
|
|
6
6
|
# An experimental class that wraps a non-shareable object, allowing multiple
|
7
7
|
# Ractors to access it concurrently.
|
8
8
|
#
|
9
|
+
# WARNING: This is a highly experimental library, and currently _not_
|
10
|
+
# recommended for production use. (As of Ruby 3.0.0, the same can be said of
|
11
|
+
# Ractors in general.)
|
12
|
+
#
|
9
13
|
# ## What is Ractor::Wrapper?
|
10
14
|
#
|
11
15
|
# Ractors for the most part cannot access objects concurrently with other
|
12
16
|
# Ractors unless the object is _shareable_ (that is, deeply immutable along
|
13
|
-
# with a few other restrictions.) If multiple Ractors need to
|
14
|
-
# resource that is stateful or otherwise not Ractor-shareable, that
|
15
|
-
# must itself be implemented and accessed as a Ractor.
|
17
|
+
# with a few other restrictions.) If multiple Ractors need to interact with a
|
18
|
+
# shared resource that is stateful or otherwise not Ractor-shareable, that
|
19
|
+
# resource must itself be implemented and accessed as a Ractor.
|
16
20
|
#
|
17
21
|
# `Ractor::Wrapper` makes it possible for such a shared resource to be
|
18
22
|
# implemented as an object and accessed using ordinary method calls. It does
|
@@ -41,7 +45,7 @@ class Ractor
|
|
41
45
|
# connection = Faraday.new "http://example.com"
|
42
46
|
# wrapper = Ractor::Wrapper.new(connection)
|
43
47
|
#
|
44
|
-
# # At this point, the connection
|
48
|
+
# # At this point, the connection object cannot be accessed directly
|
45
49
|
# # because it has been "moved" to the wrapper's internal Ractor.
|
46
50
|
# # connection.get("/whoops") # <= raises an error
|
47
51
|
#
|
@@ -76,6 +80,8 @@ class Ractor
|
|
76
80
|
# * Provides a method interface to an object running in a different Ractor.
|
77
81
|
# * Supports arbitrary method arguments and return values.
|
78
82
|
# * Supports exceptions thrown by the method.
|
83
|
+
# * Can be configured to copy or move arguments, return values, and
|
84
|
+
# exceptions, per method.
|
79
85
|
# * Can serialize method calls for non-concurrency-safe objects, or run
|
80
86
|
# methods concurrently in multiple worker threads for thread-safe objects.
|
81
87
|
# * Can gracefully shut down the wrapper and retrieve the original object.
|
@@ -106,19 +112,34 @@ class Ractor
|
|
106
112
|
# configuration is frozen once the object is constructed.)
|
107
113
|
#
|
108
114
|
# @param object [Object] The non-shareable object to wrap.
|
109
|
-
# @param threads [Integer
|
110
|
-
# Defaults to
|
115
|
+
# @param threads [Integer] The number of worker threads to run.
|
116
|
+
# Defaults to 1, which causes the worker to serialize calls.
|
111
117
|
#
|
112
|
-
def initialize(object,
|
118
|
+
def initialize(object,
|
119
|
+
threads: 1,
|
120
|
+
move: false,
|
121
|
+
move_arguments: nil,
|
122
|
+
move_return: nil,
|
123
|
+
logging: false,
|
124
|
+
name: nil)
|
125
|
+
@method_settings = {}
|
113
126
|
self.threads = threads
|
114
127
|
self.logging = logging
|
115
128
|
self.name = name
|
129
|
+
configure_method(move: move, move_arguments: move_arguments, move_return: move_return)
|
116
130
|
yield self if block_given?
|
131
|
+
@method_settings.freeze
|
117
132
|
|
118
133
|
maybe_log("Starting server")
|
119
134
|
@ractor = ::Ractor.new(name: name) { Server.new.run }
|
120
|
-
opts = {
|
121
|
-
|
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)
|
122
143
|
|
123
144
|
maybe_log("Server ready")
|
124
145
|
@stub = Stub.new(self)
|
@@ -128,28 +149,25 @@ class Ractor
|
|
128
149
|
##
|
129
150
|
# Set the number of threads to run in the wrapper. If the underlying object
|
130
151
|
# is thread-safe, this allows concurrent calls to it. If the underlying
|
131
|
-
# object is not thread-safe, you should leave this set to
|
132
|
-
#
|
133
|
-
# effectively the same as no threading.
|
152
|
+
# object is not thread-safe, you should leave this set to its default of 1,
|
153
|
+
# which effectively causes calls to be serialized.
|
134
154
|
#
|
135
155
|
# This method can be called only during an initialization block.
|
156
|
+
# All settings are frozen once the wrapper is active.
|
136
157
|
#
|
137
|
-
# @param value [Integer
|
158
|
+
# @param value [Integer]
|
138
159
|
#
|
139
160
|
def threads=(value)
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
@threads = value
|
144
|
-
else
|
145
|
-
@threads = nil
|
146
|
-
end
|
161
|
+
value = value.to_i
|
162
|
+
value = 1 if value < 1
|
163
|
+
@threads = value
|
147
164
|
end
|
148
165
|
|
149
166
|
##
|
150
167
|
# Enable or disable internal debug logging.
|
151
168
|
#
|
152
169
|
# This method can be called only during an initialization block.
|
170
|
+
# All settings are frozen once the wrapper is active.
|
153
171
|
#
|
154
172
|
# @param value [Boolean]
|
155
173
|
#
|
@@ -158,9 +176,11 @@ class Ractor
|
|
158
176
|
end
|
159
177
|
|
160
178
|
##
|
161
|
-
# Set the name of this wrapper
|
179
|
+
# Set the name of this wrapper. This is shown in logging, and is also used
|
180
|
+
# as the name of the wrapping Ractor.
|
162
181
|
#
|
163
182
|
# This method can be called only during an initialization block.
|
183
|
+
# All settings are frozen once the wrapper is active.
|
164
184
|
#
|
165
185
|
# @param value [String, nil]
|
166
186
|
#
|
@@ -168,6 +188,32 @@ class Ractor
|
|
168
188
|
@name = value ? value.to_s.freeze : nil
|
169
189
|
end
|
170
190
|
|
191
|
+
##
|
192
|
+
# Configure the move semantics for the given method (or the default
|
193
|
+
# settings if no method name is given.) That is, determine whether
|
194
|
+
# arguments, return values, and/or exceptions are copied or moved when
|
195
|
+
# communicated with the wrapper. By default, all objects are copied.
|
196
|
+
#
|
197
|
+
# This method can be called only during an initialization block.
|
198
|
+
# All settings are frozen once the wrapper is active.
|
199
|
+
#
|
200
|
+
# @param method_name [Symbol, nil] The name of the method being configured,
|
201
|
+
# or `nil` to set defaults for all methods not configured explicitly.
|
202
|
+
# @param move [Boolean] Whether to move all communication. This value, if
|
203
|
+
# given, is used if `move_arguments`, `move_return`, or
|
204
|
+
# `move_exceptions` are not set.
|
205
|
+
# @param move_arguments [Boolean] Whether to move arguments.
|
206
|
+
# @param move_return [Boolean] Whether to move return values.
|
207
|
+
#
|
208
|
+
def configure_method(method_name = nil,
|
209
|
+
move: false,
|
210
|
+
move_arguments: nil,
|
211
|
+
move_return: nil)
|
212
|
+
method_name = method_name.to_sym unless method_name.nil?
|
213
|
+
@method_settings[method_name] =
|
214
|
+
MethodSettings.new(move: move, move_arguments: move_arguments, move_return: move_return)
|
215
|
+
end
|
216
|
+
|
171
217
|
##
|
172
218
|
# Return the wrapper stub. This is an object that responds to the same
|
173
219
|
# methods as the wrapped object, providing an easy way to call a wrapper.
|
@@ -177,15 +223,14 @@ class Ractor
|
|
177
223
|
attr_reader :stub
|
178
224
|
|
179
225
|
##
|
180
|
-
# Return the number of threads used by the wrapper
|
181
|
-
# no threading.
|
226
|
+
# Return the number of threads used by the wrapper.
|
182
227
|
#
|
183
|
-
# @return [Integer
|
228
|
+
# @return [Integer]
|
184
229
|
#
|
185
230
|
attr_reader :threads
|
186
231
|
|
187
232
|
##
|
188
|
-
# Return whether logging is enabled for this wrapper
|
233
|
+
# Return whether logging is enabled for this wrapper.
|
189
234
|
#
|
190
235
|
# @return [Boolean]
|
191
236
|
#
|
@@ -199,7 +244,21 @@ class Ractor
|
|
199
244
|
attr_reader :name
|
200
245
|
|
201
246
|
##
|
202
|
-
#
|
247
|
+
# Return the method settings for the given method name. This returns the
|
248
|
+
# default method settings if the given method is not configured explicitly
|
249
|
+
# by name.
|
250
|
+
#
|
251
|
+
# @param method_name [Symbol,nil] The method name, or `nil` to return the
|
252
|
+
# defaults.
|
253
|
+
# @return [MethodSettings]
|
254
|
+
#
|
255
|
+
def method_settings(method_name)
|
256
|
+
method_name = method_name.to_sym
|
257
|
+
@method_settings[method_name] || @method_settings[nil]
|
258
|
+
end
|
259
|
+
|
260
|
+
##
|
261
|
+
# A lower-level interface for calling methods through the wrapper.
|
203
262
|
#
|
204
263
|
# @param method_name [Symbol] The name of the method to call
|
205
264
|
# @param args [arguments] The positional arguments
|
@@ -209,8 +268,9 @@ class Ractor
|
|
209
268
|
def call(method_name, *args, **kwargs)
|
210
269
|
request = Message.new(:call, data: [method_name, args, kwargs])
|
211
270
|
transaction = request.transaction
|
212
|
-
|
213
|
-
|
271
|
+
move = method_settings(method_name).move_arguments?
|
272
|
+
maybe_log("Sending method #{method_name} (move=#{move}, transaction=#{transaction})")
|
273
|
+
@ractor.send(request, move: move)
|
214
274
|
reply = ::Ractor.receive_if { |msg| msg.is_a?(Message) && msg.transaction == transaction }
|
215
275
|
case reply.type
|
216
276
|
when :result
|
@@ -241,9 +301,11 @@ class Ractor
|
|
241
301
|
end
|
242
302
|
|
243
303
|
##
|
244
|
-
#
|
245
|
-
#
|
246
|
-
#
|
304
|
+
# Retrieves the original object that was wrapped. This should be called
|
305
|
+
# only after a stop request has been issued using {#async_stop}, and may
|
306
|
+
# block until the wrapper has fully stopped.
|
307
|
+
#
|
308
|
+
# Only one ractor may call this method; any additional calls will fail.
|
247
309
|
#
|
248
310
|
# @return [Object] The original wrapped object
|
249
311
|
#
|
@@ -276,19 +338,75 @@ class Ractor
|
|
276
338
|
|
277
339
|
##
|
278
340
|
# Forward calls to {Ractor::Wrapper#call}.
|
341
|
+
# @private
|
279
342
|
#
|
280
343
|
def method_missing(name, *args, **kwargs)
|
281
344
|
@wrapper.call(name, *args, **kwargs)
|
282
345
|
end
|
283
346
|
|
347
|
+
##
|
348
|
+
# Forward respond_to queries.
|
284
349
|
# @private
|
350
|
+
#
|
285
351
|
def respond_to_missing?(name, include_all)
|
286
|
-
@wrapper.respond_to
|
352
|
+
@wrapper.call(:respond_to?, name, include_all)
|
287
353
|
end
|
288
354
|
end
|
289
355
|
|
290
|
-
|
356
|
+
##
|
357
|
+
# Settings for a method call. Specifies how a method's arguments and
|
358
|
+
# return value are communicated (i.e. copy or move semantics.)
|
359
|
+
#
|
360
|
+
class MethodSettings
|
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
|
369
|
+
|
370
|
+
##
|
371
|
+
# @return [Boolean] Whether to move arguments
|
372
|
+
#
|
373
|
+
def move_arguments?
|
374
|
+
@move_arguments
|
375
|
+
end
|
376
|
+
|
377
|
+
##
|
378
|
+
# @return [Boolean] Whether to move return values
|
379
|
+
#
|
380
|
+
def move_return?
|
381
|
+
@move_return
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
|
386
|
+
def interpret_setting(setting, default)
|
387
|
+
if setting.nil?
|
388
|
+
default ? true : false
|
389
|
+
else
|
390
|
+
setting ? true : false
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
##
|
396
|
+
# The class of all messages passed between a client Ractor and a wrapper.
|
397
|
+
# This helps the wrapper distinguish these messages from any other messages
|
398
|
+
# that might be received by a client Ractor.
|
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.)
|
407
|
+
#
|
291
408
|
class Message
|
409
|
+
# @private
|
292
410
|
def initialize(type, data: nil, transaction: nil)
|
293
411
|
@sender = ::Ractor.current
|
294
412
|
@type = type
|
@@ -297,9 +415,16 @@ class Ractor
|
|
297
415
|
freeze
|
298
416
|
end
|
299
417
|
|
418
|
+
# @private
|
300
419
|
attr_reader :type
|
420
|
+
|
421
|
+
# @private
|
301
422
|
attr_reader :sender
|
423
|
+
|
424
|
+
# @private
|
302
425
|
attr_reader :transaction
|
426
|
+
|
427
|
+
# @private
|
303
428
|
attr_reader :data
|
304
429
|
|
305
430
|
private
|
@@ -309,19 +434,34 @@ class Ractor
|
|
309
434
|
end
|
310
435
|
end
|
311
436
|
|
437
|
+
##
|
438
|
+
# This is the backend implementation of a wrapper. A Server runs within a
|
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.
|
442
|
+
#
|
443
|
+
# See the {#run} method for an overview of the Server implementation and
|
444
|
+
# lifecycle.
|
445
|
+
#
|
312
446
|
# @private
|
447
|
+
#
|
313
448
|
class Server
|
449
|
+
##
|
450
|
+
# Handle the server lifecycle, running through the following phases:
|
451
|
+
#
|
452
|
+
# * **init**: Setup and spawning of worker threads.
|
453
|
+
# * **running**: Normal operation, until a stop request is received.
|
454
|
+
# * **stopping**: Waiting for worker threads to terminate.
|
455
|
+
# * **cleanup**: Clearing out of any lingering meessages.
|
456
|
+
#
|
457
|
+
# The server returns the wrapped object, allowing one client Ractor to
|
458
|
+
# take it.
|
459
|
+
#
|
314
460
|
def run
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
maybe_log("Server started")
|
319
|
-
|
320
|
-
queue = start_threads(opts[:threads])
|
321
|
-
running_phase(queue)
|
322
|
-
stopping_phase if queue
|
461
|
+
init_phase
|
462
|
+
running_phase
|
463
|
+
stopping_phase
|
323
464
|
cleanup_phase
|
324
|
-
|
325
465
|
@object
|
326
466
|
rescue ::StandardError => e
|
327
467
|
maybe_log("Unexpected error: #{e.inspect}")
|
@@ -330,80 +470,142 @@ class Ractor
|
|
330
470
|
|
331
471
|
private
|
332
472
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
473
|
+
##
|
474
|
+
# In the **init phase**, the Server:
|
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.
|
481
|
+
#
|
482
|
+
def init_phase
|
483
|
+
opts = ::Ractor.receive
|
484
|
+
@object = opts[: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) }
|
339
495
|
end
|
340
|
-
|
341
|
-
queue
|
496
|
+
maybe_log("Server initialized")
|
342
497
|
end
|
343
498
|
|
344
|
-
|
499
|
+
##
|
500
|
+
# A worker thread repeatedly pulls a method call requests off the job
|
501
|
+
# queue, handles it, and sends back a response. It also removes the
|
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.
|
506
|
+
#
|
507
|
+
def worker_thread(worker_num)
|
345
508
|
maybe_worker_log(worker_num, "Starting")
|
346
509
|
loop do
|
347
510
|
maybe_worker_log(worker_num, "Waiting for job")
|
348
|
-
request = queue.deq
|
349
|
-
if request.nil?
|
350
|
-
break
|
351
|
-
end
|
511
|
+
request = @queue.deq
|
512
|
+
break if request.nil?
|
352
513
|
handle_method(worker_num, request)
|
514
|
+
unregister_call(request.transaction)
|
353
515
|
end
|
516
|
+
ensure
|
354
517
|
maybe_worker_log(worker_num, "Stopping")
|
518
|
+
::Ractor.current.send(Message.new(:thread_stopped, data: worker_num), move: true)
|
355
519
|
end
|
356
520
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
521
|
+
##
|
522
|
+
# In the **running phase**, the Server listens on the Ractor's inbox and
|
523
|
+
# handles messages for normal operation:
|
524
|
+
#
|
525
|
+
# * If it receives a `call` request, it adds it to the job queue from
|
526
|
+
# which a worker thread will pick it up. It also adds the request to
|
527
|
+
# a list of pending requests.
|
528
|
+
# * If it receives a `stop` request, we proceed to the stopping phase.
|
529
|
+
# * If it receives a `thread_stopped` message, that indicates one of
|
530
|
+
# the worker threads has unexpectedly stopped. We don't expect this
|
531
|
+
# to happen until the stopping phase, so if we do see it here, we
|
532
|
+
# conclude that something has gone wrong, and we proceed to the
|
533
|
+
# stopping phase.
|
534
|
+
#
|
535
|
+
def running_phase
|
364
536
|
loop do
|
365
537
|
maybe_log("Waiting for message")
|
366
|
-
request = ::Ractor.
|
538
|
+
request = ::Ractor.receive
|
539
|
+
next unless request.is_a?(Message)
|
367
540
|
case request.type
|
368
541
|
when :call
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
542
|
+
@queue.enq(request)
|
543
|
+
register_call(request)
|
544
|
+
maybe_log("Queued method #{request.data.first} (transaction=#{request.transaction})")
|
545
|
+
when :thread_stopped
|
546
|
+
maybe_log("Thread unexpectedly stopped: #{request.data}")
|
547
|
+
@thread_count -= 1
|
548
|
+
break
|
375
549
|
when :stop
|
376
550
|
maybe_log("Received stop")
|
377
|
-
queue&.close
|
378
551
|
break
|
379
552
|
end
|
380
553
|
end
|
381
554
|
end
|
382
555
|
|
556
|
+
##
|
557
|
+
# In the **stopping phase**, we close the job queue, which signals to all
|
558
|
+
# worker threads that they should finish their current task and then
|
559
|
+
# terminate. We then wait for acknowledgement messages from all workers
|
560
|
+
# before proceeding to the next phase. Any `call` requests received
|
561
|
+
# during stopping are refused (i.e. we send back an error response.) Any
|
562
|
+
# further `stop` requests are ignored.
|
563
|
+
#
|
383
564
|
def stopping_phase
|
384
|
-
|
385
|
-
|
386
|
-
|
565
|
+
@queue.close
|
566
|
+
while @thread_count.positive?
|
567
|
+
maybe_log("Waiting for message while stopping")
|
568
|
+
message = ::Ractor.receive
|
569
|
+
next unless request.is_a?(Message)
|
387
570
|
case message.type
|
388
571
|
when :call
|
389
572
|
refuse_method(message)
|
390
|
-
when :
|
391
|
-
|
573
|
+
when :thread_stopped
|
574
|
+
@thread_count -= 1
|
392
575
|
end
|
393
576
|
end
|
394
577
|
end
|
395
578
|
|
579
|
+
##
|
580
|
+
# In the **cleanup phase**, The Server closes its inbox, and iterates
|
581
|
+
# through one final time to ensure it has responded to all remaining
|
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.
|
585
|
+
#
|
396
586
|
def cleanup_phase
|
397
587
|
::Ractor.current.close_incoming
|
588
|
+
maybe_log("Checking message queue for cleanup")
|
398
589
|
loop do
|
399
|
-
maybe_log("Checking queue for cleanup")
|
400
590
|
message = ::Ractor.receive
|
401
591
|
refuse_method(message) if message.is_a?(Message) && message.type == :call
|
402
592
|
end
|
593
|
+
maybe_log("Checking current calls for cleanup")
|
594
|
+
@current_calls.each_value do |request|
|
595
|
+
refuse_method(request)
|
596
|
+
end
|
403
597
|
rescue ::Ractor::ClosedError
|
404
|
-
maybe_log("
|
598
|
+
maybe_log("Message queue is empty")
|
405
599
|
end
|
406
600
|
|
601
|
+
##
|
602
|
+
# This is called within a worker thread to handle a method call request.
|
603
|
+
# It calls the method on the wrapped object, and then sends back a
|
604
|
+
# response to the caller. If an exception was raised, it sends back an
|
605
|
+
# error response. It tries very hard always to send a response of some
|
606
|
+
# kind; if an error occurs while constructing or sending a response, it
|
607
|
+
# will catch the exception and try to send a simpler response.
|
608
|
+
#
|
407
609
|
def handle_method(worker_num, request)
|
408
610
|
method_name, args, kwargs = request.data
|
409
611
|
transaction = request.transaction
|
@@ -412,19 +614,46 @@ class Ractor
|
|
412
614
|
begin
|
413
615
|
result = @object.send(method_name, *args, **kwargs)
|
414
616
|
maybe_worker_log(worker_num, "Sending result (transaction=#{transaction})")
|
415
|
-
sender.send(Message.new(:result, data: result, transaction: transaction),
|
617
|
+
sender.send(Message.new(:result, data: result, transaction: transaction),
|
618
|
+
move: (@method_settings[method_name] || @method_settings[nil]).move_return?)
|
416
619
|
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
417
620
|
maybe_worker_log(worker_num, "Sending exception (transaction=#{transaction})")
|
418
|
-
|
621
|
+
begin
|
622
|
+
sender.send(Message.new(:error, data: e, transaction: transaction))
|
623
|
+
rescue ::StandardError
|
624
|
+
safe_error = begin
|
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))
|
630
|
+
end
|
419
631
|
end
|
420
632
|
end
|
421
633
|
|
634
|
+
##
|
635
|
+
# This is called from the main Ractor thread to report to a caller that
|
636
|
+
# the wrapper cannot handle a requested method call, likely because the
|
637
|
+
# wrapper is shutting down.
|
638
|
+
#
|
422
639
|
def refuse_method(request)
|
423
640
|
maybe_log("Refusing method call (transaction=#{message.transaction})")
|
424
641
|
error = ::Ractor::ClosedError.new
|
425
642
|
request.sender.send(Message.new(:error, data: error, transaction: message.transaction))
|
426
643
|
end
|
427
644
|
|
645
|
+
def register_call(request)
|
646
|
+
@mutex.synchronize do
|
647
|
+
@current_calls[request.transaction] = request
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
def unregister_call(transaction)
|
652
|
+
@mutex.synchronize do
|
653
|
+
@current_calls.delete(transaction)
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
428
657
|
def maybe_log(str)
|
429
658
|
return unless @logging
|
430
659
|
time = ::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%L")
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ractor-wrapper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Azuma
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: An experimental class that wraps a non-shareable object, allowing multiple
|
14
14
|
Ractors to access it concurrently.
|
@@ -28,7 +28,12 @@ files:
|
|
28
28
|
homepage: https://github.com/dazuma/ractor-wrapper
|
29
29
|
licenses:
|
30
30
|
- MIT
|
31
|
-
metadata:
|
31
|
+
metadata:
|
32
|
+
bug_tracker_uri: https://github.com/dazuma/ractor-wrapper/issues
|
33
|
+
changelog_uri: https://rubydoc.info/gems/ractor-wrapper/0.2.0/file/CHANGELOG.md
|
34
|
+
documentation_uri: https://rubydoc.info/gems/ractor-wrapper/0.2.0
|
35
|
+
homepage_uri: https://github.com/dazuma/ractor-wrapper
|
36
|
+
source_code_uri: https://github.com/dazuma/ractor-wrapper
|
32
37
|
post_install_message:
|
33
38
|
rdoc_options: []
|
34
39
|
require_paths:
|