ractor-wrapper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []