ractor-wrapper 0.1.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 +7 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.md +21 -0
- data/README.md +152 -0
- data/lib/ractor-wrapper.rb +1 -0
- data/lib/ractor/wrapper.rb +443 -0
- data/lib/ractor/wrapper/version.rb +10 -0
- metadata +51 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ba8fea27b45cfb0ee50c95d09c19c43e3aa185db4871e1e5f892551de35c33eb
|
4
|
+
data.tar.gz: ae2558e2e07b3bc74c2409c10f91a997b391709c8d1ee10157327684f4528bc5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0e76769526ea1db2f95819275b87252a29c188aaad1a1caf5d610d84b9e1cbe18344f4266c96e2274a2ff972c6faf910393be811dfa3496c0a715aeda415f7d4
|
7
|
+
data.tar.gz: ac39e5fc611ad01bedfaf2f4c065108d9d487dfd040a21ed26845d1d92968cd137aed98c96cb5a7e45ec3485e6da03d5338685521799edf221f0f836557495e5
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# License
|
2
|
+
|
3
|
+
Copyright 2021 Daniel Azuma
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
21
|
+
IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# Ractor::Wrapper
|
2
|
+
|
3
|
+
`Ractor::Wrapper` is an experimental class that wraps a non-shareable object,
|
4
|
+
allowing multiple Ractors to access it concurrently. This can make it possible
|
5
|
+
for multiple ractors to share an object such as a database connection.
|
6
|
+
|
7
|
+
## Quick start
|
8
|
+
|
9
|
+
Install ractor-wrapper as a gem, or include it in your bundle.
|
10
|
+
|
11
|
+
gem install ractor-wrapper
|
12
|
+
|
13
|
+
Require it in your code:
|
14
|
+
|
15
|
+
require "ractor/wrapper"
|
16
|
+
|
17
|
+
You can then create wrappers for objects. See the example below.
|
18
|
+
|
19
|
+
Ractor::Wrapper requires Ruby 3.0.0 or later.
|
20
|
+
|
21
|
+
WARNING: This is a highly experimental library, and not currently intended for
|
22
|
+
production use. (As of Ruby 3.0.0, the same can be said of Ractors in general.)
|
23
|
+
|
24
|
+
## About Ractor::Wrapper
|
25
|
+
|
26
|
+
Ractors for the most part cannot access objects concurrently with other
|
27
|
+
Ractors unless the object is _shareable_ (that is, deeply immutable along
|
28
|
+
with a few other restrictions.) If multiple Ractors need to access a shared
|
29
|
+
resource that is stateful or otherwise not Ractor-shareable, that resource
|
30
|
+
must itself be a Ractor.
|
31
|
+
|
32
|
+
`Ractor::Wrapper` makes it possible for such a shared resource to be
|
33
|
+
implemented as an ordinary object and accessed using ordinary method calls. It
|
34
|
+
does this by "wrapping" the object in a Ractor, and mapping method calls to
|
35
|
+
message passing. This may make it easier to implement such a resource with
|
36
|
+
a simple class rather than a full-blown Ractor with message passing, and it
|
37
|
+
may also useful for adapting existing legacy object-based implementations.
|
38
|
+
|
39
|
+
Given a shared resource object, `Ractor::Wrapper` starts a new Ractor and
|
40
|
+
"runs" the object within that Ractor. It provides you with a stub object
|
41
|
+
on which you can invoke methods. The wrapper responds to these method calls
|
42
|
+
by sending messages to the internal Ractor, which invokes the shared object
|
43
|
+
and then sends back the result. If the underlying object is thread-safe,
|
44
|
+
you can configure the wrapper to run multiple threads that can run methods
|
45
|
+
concurrently. Or, if not, the wrapper can serialize requests to the object.
|
46
|
+
|
47
|
+
### Example usage
|
48
|
+
|
49
|
+
The following example shows how to share a single `Faraday::Conection`
|
50
|
+
object among multiple Ractors. Because `Faraday::Connection` is not itself
|
51
|
+
thread-safe, this example serializes all calls to it.
|
52
|
+
|
53
|
+
require "faraday"
|
54
|
+
require "ractor/wrapper"
|
55
|
+
|
56
|
+
# Create a Faraday connection and a wrapper for it.
|
57
|
+
connection = Faraday.new "http://example.com"
|
58
|
+
wrapper = Ractor::Wrapper.new(connection)
|
59
|
+
|
60
|
+
# At this point, the connection ojbect cannot be accessed directly
|
61
|
+
# because it has been "moved" to the wrapper's internal Ractor.
|
62
|
+
# connection.get("/whoops") # <= raises an error
|
63
|
+
|
64
|
+
# However, any number of Ractors can now access it through the wrapper.
|
65
|
+
# By default, access to the object is serialized; methods will not be
|
66
|
+
# invoked concurrently. (To allow concurrent access, set up threads when
|
67
|
+
# creating the wrapper.)
|
68
|
+
r1 = Ractor.new(wrapper) do |w|
|
69
|
+
10.times do
|
70
|
+
w.stub.get("/hello")
|
71
|
+
end
|
72
|
+
:ok
|
73
|
+
end
|
74
|
+
r2 = Ractor.new(wrapper) do |w|
|
75
|
+
10.times do
|
76
|
+
w.stub.get("/ruby")
|
77
|
+
end
|
78
|
+
:ok
|
79
|
+
end
|
80
|
+
|
81
|
+
# Wait for the two above Ractors to finish.
|
82
|
+
r1.take
|
83
|
+
r2.take
|
84
|
+
|
85
|
+
# After you stop the wrapper, you can retrieve the underlying
|
86
|
+
# connection object and access it directly again.
|
87
|
+
wrapper.async_stop
|
88
|
+
connection = wrapper.recover_object
|
89
|
+
connection.get("/finally")
|
90
|
+
|
91
|
+
### Features
|
92
|
+
|
93
|
+
* Provides a method interface to an object running in a different Ractor.
|
94
|
+
* Supports arbitrary method arguments and return values.
|
95
|
+
* Supports exceptions thrown by the method.
|
96
|
+
* Can serialize method calls for non-concurrency-safe objects, or run
|
97
|
+
methods concurrently in multiple worker threads for thread-safe objects.
|
98
|
+
* Can gracefully shut down the wrapper and retrieve the original object.
|
99
|
+
|
100
|
+
### Caveats
|
101
|
+
|
102
|
+
Ractor::Wrapper is subject to some limitations (and bugs) of Ractors, as of
|
103
|
+
Ruby 3.0.0.
|
104
|
+
|
105
|
+
* You cannot pass blocks to wrapped methods.
|
106
|
+
* Certain types cannot be used as method arguments or return values
|
107
|
+
because Ractor does not allow them to be moved between Ractors. These
|
108
|
+
include threads, procs, backtraces, and a few others.
|
109
|
+
* You can call wrapper methods from multiple Ractors concurrently, but
|
110
|
+
you cannot call them from multiple Threads within a single Ractor.
|
111
|
+
(This is due to https://bugs.ruby-lang.org/issues/17624)
|
112
|
+
* If you close the incoming port on a Ractor, it will no longer be able
|
113
|
+
to call out via a wrapper. If you close its incoming port while a call
|
114
|
+
is currently pending, that call may hang. (This is due to
|
115
|
+
https://bugs.ruby-lang.org/issues/17617)
|
116
|
+
|
117
|
+
## Contributing
|
118
|
+
|
119
|
+
Development is done in GitHub at https://github.com/dazuma/ractor-wrapper.
|
120
|
+
|
121
|
+
* To file issues: https://github.com/dazuma/ractor-wrapper/issues.
|
122
|
+
* For questions and discussion, please do not file an issue. Instead, use the
|
123
|
+
discussions feature: https://github.com/dazuma/ractor-wrapper/discussions.
|
124
|
+
* Pull requests are welcome, but the library is highly experimental at this
|
125
|
+
stage, and I recommend discussing features or design changes first before
|
126
|
+
implementing.
|
127
|
+
|
128
|
+
The library uses [toys](https://dazuma.github.io/toys) for testing and CI. To
|
129
|
+
run the test suite, `gem install toys` and then run `toys ci`. You can also run
|
130
|
+
unit tests, rubocop, and builds independently.
|
131
|
+
|
132
|
+
## License
|
133
|
+
|
134
|
+
Copyright 2021 Daniel Azuma
|
135
|
+
|
136
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
137
|
+
of this software and associated documentation files (the "Software"), to deal
|
138
|
+
in the Software without restriction, including without limitation the rights
|
139
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
140
|
+
copies of the Software, and to permit persons to whom the Software is
|
141
|
+
furnished to do so, subject to the following conditions:
|
142
|
+
|
143
|
+
The above copyright notice and this permission notice shall be included in
|
144
|
+
all copies or substantial portions of the Software.
|
145
|
+
|
146
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
147
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
148
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
149
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
150
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
151
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
152
|
+
IN THE SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
require "ractor/wrapper"
|
@@ -0,0 +1,443 @@
|
|
1
|
+
##
|
2
|
+
# See ruby-doc.org for info on Ractors.
|
3
|
+
#
|
4
|
+
class Ractor
|
5
|
+
##
|
6
|
+
# An experimental class that wraps a non-shareable object, allowing multiple
|
7
|
+
# Ractors to access it concurrently.
|
8
|
+
#
|
9
|
+
# ## What is Ractor::Wrapper?
|
10
|
+
#
|
11
|
+
# Ractors for the most part cannot access objects concurrently with other
|
12
|
+
# Ractors unless the object is _shareable_ (that is, deeply immutable along
|
13
|
+
# with a few other restrictions.) If multiple Ractors need to access a shared
|
14
|
+
# resource that is stateful or otherwise not Ractor-shareable, that resource
|
15
|
+
# must itself be implemented and accessed as a Ractor.
|
16
|
+
#
|
17
|
+
# `Ractor::Wrapper` makes it possible for such a shared resource to be
|
18
|
+
# implemented as an object and accessed using ordinary method calls. It does
|
19
|
+
# this by "wrapping" the object in a Ractor, and mapping method calls to
|
20
|
+
# message passing. This may make it easier to implement such a resource with
|
21
|
+
# a simple class rather than a full-blown Ractor with message passing, and it
|
22
|
+
# may also useful for adapting existing legacy object-based implementations.
|
23
|
+
#
|
24
|
+
# Given a shared resource object, `Ractor::Wrapper` starts a new Ractor and
|
25
|
+
# "runs" the object within that Ractor. It provides you with a stub object
|
26
|
+
# on which you can invoke methods. The wrapper responds to these method calls
|
27
|
+
# by sending messages to the internal Ractor, which invokes the shared object
|
28
|
+
# and then sends back the result. If the underlying object is thread-safe,
|
29
|
+
# you can configure the wrapper to run multiple threads that can run methods
|
30
|
+
# concurrently. Or, if not, the wrapper can serialize requests to the object.
|
31
|
+
#
|
32
|
+
# ## Example usage
|
33
|
+
#
|
34
|
+
# The following example shows how to share a single `Faraday::Conection`
|
35
|
+
# object among multiple Ractors. Because `Faraday::Connection` is not itself
|
36
|
+
# thread-safe, this example serializes all calls to it.
|
37
|
+
#
|
38
|
+
# require "faraday"
|
39
|
+
#
|
40
|
+
# # Create a Faraday connection and a wrapper for it.
|
41
|
+
# connection = Faraday.new "http://example.com"
|
42
|
+
# wrapper = Ractor::Wrapper.new(connection)
|
43
|
+
#
|
44
|
+
# # At this point, the connection ojbect cannot be accessed directly
|
45
|
+
# # because it has been "moved" to the wrapper's internal Ractor.
|
46
|
+
# # connection.get("/whoops") # <= raises an error
|
47
|
+
#
|
48
|
+
# # However, any number of Ractors can now access it through the wrapper.
|
49
|
+
# # By default, access to the object is serialized; methods will not be
|
50
|
+
# # invoked concurrently.
|
51
|
+
# r1 = Ractor.new(wrapper) do |w|
|
52
|
+
# 10.times do
|
53
|
+
# w.stub.get("/hello")
|
54
|
+
# end
|
55
|
+
# :ok
|
56
|
+
# end
|
57
|
+
# r2 = Ractor.new(wrapper) do |w|
|
58
|
+
# 10.times do
|
59
|
+
# w.stub.get("/ruby")
|
60
|
+
# end
|
61
|
+
# :ok
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# # Wait for the two above Ractors to finish.
|
65
|
+
# r1.take
|
66
|
+
# r2.take
|
67
|
+
#
|
68
|
+
# # After you stop the wrapper, you can retrieve the underlying
|
69
|
+
# # connection object and access it directly again.
|
70
|
+
# wrapper.async_stop
|
71
|
+
# connection = wrapper.recover_object
|
72
|
+
# connection.get("/finally")
|
73
|
+
#
|
74
|
+
# ## Features
|
75
|
+
#
|
76
|
+
# * Provides a method interface to an object running in a different Ractor.
|
77
|
+
# * Supports arbitrary method arguments and return values.
|
78
|
+
# * Supports exceptions thrown by the method.
|
79
|
+
# * Can serialize method calls for non-concurrency-safe objects, or run
|
80
|
+
# methods concurrently in multiple worker threads for thread-safe objects.
|
81
|
+
# * Can gracefully shut down the wrapper and retrieve the original object.
|
82
|
+
#
|
83
|
+
# ## Caveats
|
84
|
+
#
|
85
|
+
# Ractor::Wrapper is subject to some limitations (and bugs) of Ractors, as of
|
86
|
+
# Ruby 3.0.0.
|
87
|
+
#
|
88
|
+
# * You cannot pass blocks to wrapped methods.
|
89
|
+
# * Certain types cannot be used as method arguments or return values
|
90
|
+
# because Ractor does not allow them to be moved between Ractors. These
|
91
|
+
# include threads, procs, backtraces, and a few others.
|
92
|
+
# * You can call wrapper methods from multiple Ractors concurrently, but
|
93
|
+
# you cannot call them from multiple Threads within a single Ractor.
|
94
|
+
# (This is due to https://bugs.ruby-lang.org/issues/17624)
|
95
|
+
# * If you close the incoming port on a Ractor, it will no longer be able
|
96
|
+
# to call out via a wrapper. If you close its incoming port while a call
|
97
|
+
# is currently pending, that call may hang. (This is due to
|
98
|
+
# https://bugs.ruby-lang.org/issues/17617)
|
99
|
+
#
|
100
|
+
class Wrapper
|
101
|
+
##
|
102
|
+
# Create a wrapper around the given object.
|
103
|
+
#
|
104
|
+
# If you pass an optional block, the wrapper itself will be yielded to it
|
105
|
+
# at which time you can set additional configuration options. (The
|
106
|
+
# configuration is frozen once the object is constructed.)
|
107
|
+
#
|
108
|
+
# @param object [Object] The non-shareable object to wrap.
|
109
|
+
# @param threads [Integer,nil] The number of worker threads to run.
|
110
|
+
# Defaults to `nil`, which causes the worker to serialize calls.
|
111
|
+
#
|
112
|
+
def initialize(object, threads: nil, logging: false, name: nil)
|
113
|
+
self.threads = threads
|
114
|
+
self.logging = logging
|
115
|
+
self.name = name
|
116
|
+
yield self if block_given?
|
117
|
+
|
118
|
+
maybe_log("Starting server")
|
119
|
+
@ractor = ::Ractor.new(name: name) { Server.new.run }
|
120
|
+
opts = {name: @name, threads: @threads, logging: @logging}
|
121
|
+
@ractor.send([object, opts], move: true)
|
122
|
+
|
123
|
+
maybe_log("Server ready")
|
124
|
+
@stub = Stub.new(self)
|
125
|
+
freeze
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Set the number of threads to run in the wrapper. If the underlying object
|
130
|
+
# is thread-safe, this allows concurrent calls to it. If the underlying
|
131
|
+
# object is not thread-safe, you should leave this set to `nil`, which will
|
132
|
+
# cause calls to be serialized. Setting the thread count to 1 is
|
133
|
+
# effectively the same as no threading.
|
134
|
+
#
|
135
|
+
# This method can be called only during an initialization block.
|
136
|
+
#
|
137
|
+
# @param value [Integer,nil]
|
138
|
+
#
|
139
|
+
def threads=(value)
|
140
|
+
if value
|
141
|
+
value = value.to_i
|
142
|
+
value = 1 if value < 1
|
143
|
+
@threads = value
|
144
|
+
else
|
145
|
+
@threads = nil
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# Enable or disable internal debug logging.
|
151
|
+
#
|
152
|
+
# This method can be called only during an initialization block.
|
153
|
+
#
|
154
|
+
# @param value [Boolean]
|
155
|
+
#
|
156
|
+
def logging=(value)
|
157
|
+
@logging = value ? true : false
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Set the name of this wrapper, shown in logging.
|
162
|
+
#
|
163
|
+
# This method can be called only during an initialization block.
|
164
|
+
#
|
165
|
+
# @param value [String, nil]
|
166
|
+
#
|
167
|
+
def name=(value)
|
168
|
+
@name = value ? value.to_s.freeze : nil
|
169
|
+
end
|
170
|
+
|
171
|
+
##
|
172
|
+
# Return the wrapper stub. This is an object that responds to the same
|
173
|
+
# methods as the wrapped object, providing an easy way to call a wrapper.
|
174
|
+
#
|
175
|
+
# @return [Ractor::Wrapper::Stub]
|
176
|
+
#
|
177
|
+
attr_reader :stub
|
178
|
+
|
179
|
+
##
|
180
|
+
# Return the number of threads used by the wrapper, or `nil` for no
|
181
|
+
# no threading.
|
182
|
+
#
|
183
|
+
# @return [Integer, nil]
|
184
|
+
#
|
185
|
+
attr_reader :threads
|
186
|
+
|
187
|
+
##
|
188
|
+
# Return whether logging is enabled for this wrapper
|
189
|
+
#
|
190
|
+
# @return [Boolean]
|
191
|
+
#
|
192
|
+
attr_reader :logging
|
193
|
+
|
194
|
+
##
|
195
|
+
# Return the name of this wrapper.
|
196
|
+
#
|
197
|
+
# @return [String, nil]
|
198
|
+
#
|
199
|
+
attr_reader :name
|
200
|
+
|
201
|
+
##
|
202
|
+
# A lower-level interface for calling the wrapper.
|
203
|
+
#
|
204
|
+
# @param method_name [Symbol] The name of the method to call
|
205
|
+
# @param args [arguments] The positional arguments
|
206
|
+
# @param kwargs [keywords] The keyword arguments
|
207
|
+
# @return [Object] The return value
|
208
|
+
#
|
209
|
+
def call(method_name, *args, **kwargs)
|
210
|
+
request = Message.new(:call, data: [method_name, args, kwargs])
|
211
|
+
transaction = request.transaction
|
212
|
+
maybe_log("Sending method #{method_name} (transaction=#{transaction})")
|
213
|
+
@ractor.send(request, move: true)
|
214
|
+
reply = ::Ractor.receive_if { |msg| msg.is_a?(Message) && msg.transaction == transaction }
|
215
|
+
case reply.type
|
216
|
+
when :result
|
217
|
+
maybe_log("Received result for method #{method_name} (transaction=#{transaction})")
|
218
|
+
reply.data
|
219
|
+
when :error
|
220
|
+
maybe_log("Received exception for method #{method_name} (transaction=#{transaction})")
|
221
|
+
raise reply.data
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
##
|
226
|
+
# Request that the wrapper stop. All currently running calls will complete
|
227
|
+
# before the wrapper actually terminates. However, any new calls will fail.
|
228
|
+
#
|
229
|
+
# This metnod is idempotent and can be called multiple times (even from
|
230
|
+
# different ractors).
|
231
|
+
#
|
232
|
+
# @return [self]
|
233
|
+
#
|
234
|
+
def async_stop
|
235
|
+
maybe_log("Stopping #{name}")
|
236
|
+
@ractor.send(Message.new(:stop))
|
237
|
+
self
|
238
|
+
rescue ::Ractor::ClosedError
|
239
|
+
# Ignore to allow stops to be idempotent.
|
240
|
+
self
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# Return the original object that was wrapped. The object is returned after
|
245
|
+
# the wrapper finishes stopping. Only one ractor may call this method; any
|
246
|
+
# additional calls will fail.
|
247
|
+
#
|
248
|
+
# @return [Object] The original wrapped object
|
249
|
+
#
|
250
|
+
def recovered_object
|
251
|
+
@ractor.take
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def maybe_log(str)
|
257
|
+
return unless logging
|
258
|
+
time = ::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%L")
|
259
|
+
$stderr.puts("[#{time} Ractor::Wrapper/#{name}]: #{str}")
|
260
|
+
$stderr.flush
|
261
|
+
end
|
262
|
+
|
263
|
+
##
|
264
|
+
# A stub that forwards calls to a wrapper.
|
265
|
+
#
|
266
|
+
class Stub
|
267
|
+
##
|
268
|
+
# Create a stub given a wrapper.
|
269
|
+
#
|
270
|
+
# @param wrapper [Ractor::Wrapper]
|
271
|
+
#
|
272
|
+
def initialize(wrapper)
|
273
|
+
@wrapper = wrapper
|
274
|
+
freeze
|
275
|
+
end
|
276
|
+
|
277
|
+
##
|
278
|
+
# Forward calls to {Ractor::Wrapper#call}.
|
279
|
+
#
|
280
|
+
def method_missing(name, *args, **kwargs)
|
281
|
+
@wrapper.call(name, *args, **kwargs)
|
282
|
+
end
|
283
|
+
|
284
|
+
# @private
|
285
|
+
def respond_to_missing?(name, include_all)
|
286
|
+
@wrapper.respond_to?(name, include_all)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# @private
|
291
|
+
class Message
|
292
|
+
def initialize(type, data: nil, transaction: nil)
|
293
|
+
@sender = ::Ractor.current
|
294
|
+
@type = type
|
295
|
+
@data = data
|
296
|
+
@transaction = transaction || new_transaction
|
297
|
+
freeze
|
298
|
+
end
|
299
|
+
|
300
|
+
attr_reader :type
|
301
|
+
attr_reader :sender
|
302
|
+
attr_reader :transaction
|
303
|
+
attr_reader :data
|
304
|
+
|
305
|
+
private
|
306
|
+
|
307
|
+
def new_transaction
|
308
|
+
::Random.rand(7958661109946400884391936).to_s(36).freeze
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# @private
|
313
|
+
class Server
|
314
|
+
def run
|
315
|
+
@object, opts = ::Ractor.receive
|
316
|
+
@logging = opts[:logging]
|
317
|
+
@name = opts[:name]
|
318
|
+
maybe_log("Server started")
|
319
|
+
|
320
|
+
queue = start_threads(opts[:threads])
|
321
|
+
running_phase(queue)
|
322
|
+
stopping_phase if queue
|
323
|
+
cleanup_phase
|
324
|
+
|
325
|
+
@object
|
326
|
+
rescue ::StandardError => e
|
327
|
+
maybe_log("Unexpected error: #{e.inspect}")
|
328
|
+
@object
|
329
|
+
end
|
330
|
+
|
331
|
+
private
|
332
|
+
|
333
|
+
def start_threads(thread_count)
|
334
|
+
return nil unless thread_count
|
335
|
+
queue = ::Queue.new
|
336
|
+
maybe_log("Spawning #{thread_count} threads")
|
337
|
+
threads = (1..thread_count).map do |worker_num|
|
338
|
+
::Thread.new { worker_thread(worker_num, queue) }
|
339
|
+
end
|
340
|
+
::Thread.new { monitor_thread(threads) }
|
341
|
+
queue
|
342
|
+
end
|
343
|
+
|
344
|
+
def worker_thread(worker_num, queue)
|
345
|
+
maybe_worker_log(worker_num, "Starting")
|
346
|
+
loop do
|
347
|
+
maybe_worker_log(worker_num, "Waiting for job")
|
348
|
+
request = queue.deq
|
349
|
+
if request.nil?
|
350
|
+
break
|
351
|
+
end
|
352
|
+
handle_method(worker_num, request)
|
353
|
+
end
|
354
|
+
maybe_worker_log(worker_num, "Stopping")
|
355
|
+
end
|
356
|
+
|
357
|
+
def monitor_thread(workers)
|
358
|
+
workers.each(&:join)
|
359
|
+
maybe_log("All workers finished")
|
360
|
+
::Ractor.current.send(Message.new(:threads_stopped))
|
361
|
+
end
|
362
|
+
|
363
|
+
def running_phase(queue)
|
364
|
+
loop do
|
365
|
+
maybe_log("Waiting for message")
|
366
|
+
request = ::Ractor.receive_if { |msg| msg.is_a?(Message) }
|
367
|
+
case request.type
|
368
|
+
when :call
|
369
|
+
if queue
|
370
|
+
queue.enq(request)
|
371
|
+
maybe_log("Queued method #{request.data.first} (transaction=#{request.transaction})")
|
372
|
+
else
|
373
|
+
handle_method(0, request)
|
374
|
+
end
|
375
|
+
when :stop
|
376
|
+
maybe_log("Received stop")
|
377
|
+
queue&.close
|
378
|
+
break
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def stopping_phase
|
384
|
+
loop do
|
385
|
+
maybe_log("Waiting for message")
|
386
|
+
message = ::Ractor.receive_if { |msg| msg.is_a?(Message) }
|
387
|
+
case message.type
|
388
|
+
when :call
|
389
|
+
refuse_method(message)
|
390
|
+
when :threads_stopped
|
391
|
+
break
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
def cleanup_phase
|
397
|
+
::Ractor.current.close_incoming
|
398
|
+
loop do
|
399
|
+
maybe_log("Checking queue for cleanup")
|
400
|
+
message = ::Ractor.receive
|
401
|
+
refuse_method(message) if message.is_a?(Message) && message.type == :call
|
402
|
+
end
|
403
|
+
rescue ::Ractor::ClosedError
|
404
|
+
maybe_log("Queue is empty")
|
405
|
+
end
|
406
|
+
|
407
|
+
def handle_method(worker_num, request)
|
408
|
+
method_name, args, kwargs = request.data
|
409
|
+
transaction = request.transaction
|
410
|
+
sender = request.sender
|
411
|
+
maybe_worker_log(worker_num, "Running method #{method_name} (transaction=#{transaction})")
|
412
|
+
begin
|
413
|
+
result = @object.send(method_name, *args, **kwargs)
|
414
|
+
maybe_worker_log(worker_num, "Sending result (transaction=#{transaction})")
|
415
|
+
sender.send(Message.new(:result, data: result, transaction: transaction), move: true)
|
416
|
+
rescue ::Exception => e # rubocop:disable Lint/RescueException
|
417
|
+
maybe_worker_log(worker_num, "Sending exception (transaction=#{transaction})")
|
418
|
+
sender.send(Message.new(:error, data: e, transaction: transaction))
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def refuse_method(request)
|
423
|
+
maybe_log("Refusing method call (transaction=#{message.transaction})")
|
424
|
+
error = ::Ractor::ClosedError.new
|
425
|
+
request.sender.send(Message.new(:error, data: error, transaction: message.transaction))
|
426
|
+
end
|
427
|
+
|
428
|
+
def maybe_log(str)
|
429
|
+
return unless @logging
|
430
|
+
time = ::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%L")
|
431
|
+
$stderr.puts("[#{time} Ractor::Wrapper/#{@name} Server]: #{str}")
|
432
|
+
$stderr.flush
|
433
|
+
end
|
434
|
+
|
435
|
+
def maybe_worker_log(worker_num, str)
|
436
|
+
return unless @logging
|
437
|
+
time = ::Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%L")
|
438
|
+
$stderr.puts("[#{time} Ractor::Wrapper/#{@name} Worker/#{worker_num}]: #{str}")
|
439
|
+
$stderr.flush
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
metadata
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ractor-wrapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Azuma
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-02 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: An experimental class that wraps a non-shareable object, allowing multiple
|
14
|
+
Ractors to access it concurrently.
|
15
|
+
email:
|
16
|
+
- dazuma@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".yardopts"
|
22
|
+
- CHANGELOG.md
|
23
|
+
- LICENSE.md
|
24
|
+
- README.md
|
25
|
+
- lib/ractor-wrapper.rb
|
26
|
+
- lib/ractor/wrapper.rb
|
27
|
+
- lib/ractor/wrapper/version.rb
|
28
|
+
homepage: https://github.com/dazuma/ractor-wrapper
|
29
|
+
licenses:
|
30
|
+
- MIT
|
31
|
+
metadata: {}
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
requirements: []
|
47
|
+
rubygems_version: 3.2.3
|
48
|
+
signing_key:
|
49
|
+
specification_version: 4
|
50
|
+
summary: A Ractor wrapper for a non-shareable object.
|
51
|
+
test_files: []
|