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 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
@@ -0,0 +1,10 @@
1
+ --no-private
2
+ --title=Ractor::Wrapper
3
+ --markup=markdown
4
+ --markup-provider redcarpet
5
+ --main=README.md
6
+ ./lib/ractor/wrapper.rb
7
+ -
8
+ README.md
9
+ LICENSE.md
10
+ CHANGELOG.md
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Release History
2
+
3
+ ### v0.1.0 / 2021-03-02
4
+
5
+ * Initial release. HIGHLY EXPERIMENTAL.
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
@@ -0,0 +1,10 @@
1
+ class Ractor
2
+ class Wrapper
3
+ ##
4
+ # The version of the ractor-wrapper gem
5
+ #
6
+ # @return [String]
7
+ #
8
+ VERSION = "0.1.0".freeze
9
+ end
10
+ 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: []