ractor-wrapper 0.4.0 → 0.5.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/.yardopts +1 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +5 -2
- data/DESIGN.md +1024 -0
- data/README.md +59 -42
- data/lib/ractor/wrapper/version.rb +1 -1
- data/lib/ractor/wrapper.rb +576 -66
- metadata +4 -3
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ractor::Wrapper
|
|
2
2
|
|
|
3
|
-
Ractor::Wrapper is an experimental class that wraps a non-shareable object in
|
|
3
|
+
`Ractor::Wrapper` is an experimental class that wraps a non-shareable object in
|
|
4
4
|
an actor, allowing multiple Ractors to access it concurrently.
|
|
5
5
|
|
|
6
6
|
**WARNING:** This is an experimental library, and currently _not_ recommended
|
|
@@ -23,9 +23,9 @@ require "ractor/wrapper"
|
|
|
23
23
|
|
|
24
24
|
You can then create wrappers for objects. See the example below.
|
|
25
25
|
|
|
26
|
-
Ractor::Wrapper requires Ruby 4.0.0 or later.
|
|
26
|
+
`Ractor::Wrapper` requires Ruby 4.0.0 or later.
|
|
27
27
|
|
|
28
|
-
## What is Ractor::Wrapper
|
|
28
|
+
## What is `Ractor::Wrapper`?
|
|
29
29
|
|
|
30
30
|
For the most part, unless an object is _shareable_, which generally means
|
|
31
31
|
deeply immutable along with a few other restrictions, it cannot be accessed
|
|
@@ -43,7 +43,7 @@ such as a database connection.
|
|
|
43
43
|
| | fails | |
|
|
44
44
|
+-------------------+ +----------------+
|
|
45
45
|
|
|
46
|
-
Ractor::Wrapper makes it possible for an ordinary non-shareable object to
|
|
46
|
+
`Ractor::Wrapper` makes it possible for an ordinary non-shareable object to
|
|
47
47
|
be accessed from multiple Ractors. It does this by "wrapping" the object with
|
|
48
48
|
a shareable proxy.
|
|
49
49
|
|
|
@@ -66,13 +66,13 @@ fully transparent. Behind the scenes, the wrapper "runs" the wrapped object in
|
|
|
66
66
|
a controlled single-Ractor environment, and uses port messaging to communicate
|
|
67
67
|
method calls, arguments, and return values between Ractors.
|
|
68
68
|
|
|
69
|
-
Ractor::Wrapper can be used to adapt non-shareable objects to a multi-Ractor
|
|
69
|
+
`Ractor::Wrapper` can be used to adapt non-shareable objects to a multi-Ractor
|
|
70
70
|
world. It can also be used to implement a simple actor by writing a "plain"
|
|
71
71
|
Ruby object and wrapping it with a Ractor.
|
|
72
72
|
|
|
73
73
|
## Examples
|
|
74
74
|
|
|
75
|
-
Below are some illustrative examples showing how to use Ractor::Wrapper
|
|
75
|
+
Below are some illustrative examples showing how to use `Ractor::Wrapper`.
|
|
76
76
|
|
|
77
77
|
### Net::HTTP example
|
|
78
78
|
|
|
@@ -195,9 +195,9 @@ wrapper.join
|
|
|
195
195
|
|
|
196
196
|
### Simple actor example
|
|
197
197
|
|
|
198
|
-
The following example demonstrates how to use Ractor::Wrapper to implement an
|
|
198
|
+
The following example demonstrates how to use `Ractor::Wrapper` to implement an
|
|
199
199
|
actor as a plain Ruby object. Focus on writing functionality as methods, and
|
|
200
|
-
let Ractor::Wrapper handle all the messaging logic.
|
|
200
|
+
let `Ractor::Wrapper` handle all the messaging logic.
|
|
201
201
|
|
|
202
202
|
```ruby
|
|
203
203
|
# Simple actor example
|
|
@@ -246,9 +246,9 @@ calc_actor.join
|
|
|
246
246
|
|
|
247
247
|
## Configuring a wrapper
|
|
248
248
|
|
|
249
|
-
Ractor::Wrapper supports a fair amount of configuration, which may be needed
|
|
250
|
-
order to ensure good behavior of the wrapped object. You can configure many
|
|
251
|
-
aspects of Ractor::Wrapper by passing keyword arguments to its constructor.
|
|
249
|
+
`Ractor::Wrapper` supports a fair amount of configuration, which may be needed
|
|
250
|
+
in order to ensure good behavior of the wrapped object. You can configure many
|
|
251
|
+
aspects of `Ractor::Wrapper` by passing keyword arguments to its constructor.
|
|
252
252
|
Alternatively, you can pass a block to the constructor; the constructor will
|
|
253
253
|
yield a configuration interface to your block, letting you configure the
|
|
254
254
|
wrapper's behavior in detail.
|
|
@@ -340,7 +340,7 @@ pending method calls, and putting the wrapper in a state where it will refuse
|
|
|
340
340
|
new calls. Any additional method calls will cause a
|
|
341
341
|
`Ractor::Wrapper::StoppedError` to be raised.
|
|
342
342
|
|
|
343
|
-
Ractor::Wrapper also provides a `join` method that can be called to wait for
|
|
343
|
+
`Ractor::Wrapper` also provides a `join` method that can be called to wait for
|
|
344
344
|
the wrapper to complete its shutdown.
|
|
345
345
|
|
|
346
346
|
### Wrapped object access
|
|
@@ -361,12 +361,12 @@ Ractor that subsequently requests the object will get an exception instead.
|
|
|
361
361
|
|
|
362
362
|
In "current Ractor" mode, the object will never have been moved to a different
|
|
363
363
|
Ractor, so any pre-existing references (in the original Ractor) will still be
|
|
364
|
-
valid. In this case, `recover_object` is not necessary and will
|
|
365
|
-
|
|
364
|
+
valid. In this case, `recover_object` is not necessary and will raise an
|
|
365
|
+
exception if called.
|
|
366
366
|
|
|
367
367
|
### Error handling
|
|
368
368
|
|
|
369
|
-
Ractor::Wrapper provides fairly robust handling of errors. If a method call
|
|
369
|
+
`Ractor::Wrapper` provides fairly robust handling of errors. If a method call
|
|
370
370
|
raises an exception, the exception will be passed back to the caller and raised
|
|
371
371
|
there. In the unlikely event that the wrapper itself crashes, it goes through a
|
|
372
372
|
very thorough clean-up process and makes every effort to shut down gracefully,
|
|
@@ -380,7 +380,7 @@ a common pattern in Ruby and is used to allow "chaining" interfaces. However,
|
|
|
380
380
|
you generally cannot return `self` from a wrapped object because, depending on
|
|
381
381
|
the communication configuration, you'll either get a *copy* of `self`, or
|
|
382
382
|
you'll *move* the object out of the wrapper, thus breaking the wrapper. Thus,
|
|
383
|
-
Ractor::Wrapper explicitly detects when methods return `self`, and instead
|
|
383
|
+
`Ractor::Wrapper` explicitly detects when methods return `self`, and instead
|
|
384
384
|
replaces it with the wrapper's stub object. The stub is shareable, and designed
|
|
385
385
|
to have the same usage as the original object, so this should work for most use
|
|
386
386
|
cases.
|
|
@@ -388,10 +388,10 @@ cases.
|
|
|
388
388
|
## Known issues
|
|
389
389
|
|
|
390
390
|
Ractors are in general somewhat "bolted-on" to Ruby, and there are a lot of
|
|
391
|
-
caveats to their use. This also applies to Ractor::Wrapper
|
|
391
|
+
caveats to their use. This also applies to `Ractor::Wrapper`, which itself is
|
|
392
392
|
essentially a workaround to the fact that Ruby has a lot of use cases that
|
|
393
393
|
simply don't play well in a Ractor world. Here we'll discuss some of the
|
|
394
|
-
caveats and known issues with Ractor::Wrapper
|
|
394
|
+
caveats and known issues with `Ractor::Wrapper`.
|
|
395
395
|
|
|
396
396
|
### Data communication issues
|
|
397
397
|
|
|
@@ -407,14 +407,14 @@ One particular case of note is exception objects, which one might expect to be
|
|
|
407
407
|
shareable, but are not. Furthermore, they cannot be moved, and even copying an
|
|
408
408
|
exception has issues (in particular the backtrace of a copy gets cleared out).
|
|
409
409
|
See https://bugs.ruby-lang.org/issues/21818 for more info. When a method raises
|
|
410
|
-
an exception, Ractor::Wrapper communicates that exception via copying, which
|
|
410
|
+
an exception, `Ractor::Wrapper` communicates that exception via copying, which
|
|
411
411
|
means that currently backtraces will not be present.
|
|
412
412
|
|
|
413
413
|
### Blocks
|
|
414
414
|
|
|
415
|
-
Ruby blocks pose particular challenges for Ractor::Wrapper because of their
|
|
415
|
+
Ruby blocks pose particular challenges for `Ractor::Wrapper` because of their
|
|
416
416
|
semantics and some of their common usage patterns. We've already seen above
|
|
417
|
-
that Ractor::Wrapper can run them either in the caller's context or in the
|
|
417
|
+
that `Ractor::Wrapper` can run them either in the caller's context or in the
|
|
418
418
|
wrapped object's context, which may limit what the block can do. Additionally,
|
|
419
419
|
the following restrictions apply to blocks:
|
|
420
420
|
|
|
@@ -432,27 +432,44 @@ through a wrapper.
|
|
|
432
432
|
In Ruby, it is legal (although not considered very good practice) to do a
|
|
433
433
|
non-local `return` from inside a block. Assuming the block isn't being defined
|
|
434
434
|
via a lambda, this causes a return from the method *surrounding* the call that
|
|
435
|
-
includes the block. Ractor::Wrapper cannot reproduce this behavior.
|
|
436
|
-
to `return` within a block that was passed to Ractor::Wrapper will
|
|
437
|
-
exception.
|
|
438
|
-
|
|
439
|
-
###
|
|
440
|
-
|
|
441
|
-
One final known
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
435
|
+
includes the block. However, `Ractor::Wrapper` cannot reproduce this behavior.
|
|
436
|
+
Attempting to `return` within a block that was passed to `Ractor::Wrapper` will
|
|
437
|
+
result in an exception.
|
|
438
|
+
|
|
439
|
+
### Block re-entrancy in a separate Thread/Fiber
|
|
440
|
+
|
|
441
|
+
One final known corner case has to do with block re-entrancy, i.e. calling a
|
|
442
|
+
method *from within a block passed to another call to the same object*. This
|
|
443
|
+
would mean that there are two "active" method calls to the object at once: one
|
|
444
|
+
made while another is "suspended" because it has yielded to the block.
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
stub.method_with_block do
|
|
448
|
+
stub.another_method
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
In most cases, the wrapper handles this case properly, even in sequential mode
|
|
453
|
+
when it has no concurrency. Internally, it uses fibers to track each method
|
|
454
|
+
call, and it yields the fiber when yielding to a block. However, if the
|
|
455
|
+
"internal" method call itself is done in a separate Fiber or Thread, this would
|
|
456
|
+
break the internal fiber tracking. In such a case, the wrapper falls back to a
|
|
457
|
+
"blocking" model where it depends on concurrency to handle the simultaneous
|
|
458
|
+
method calls. If run in sequential mode, or if not enough worker threads are
|
|
459
|
+
available, this can deadlock.
|
|
460
|
+
|
|
461
|
+
```ruby
|
|
462
|
+
# Can deadlock if the wrapper was created in sequential mode
|
|
463
|
+
stub.method_with_block do
|
|
464
|
+
t = Thread.new do
|
|
465
|
+
stub.another_method
|
|
466
|
+
end
|
|
467
|
+
t.join
|
|
468
|
+
end
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
To avoid this issue, do not call methods on the same wrapper from within a
|
|
472
|
+
separate Thread or Fiber spawned inside a block passed to that wrapper.
|
|
456
473
|
|
|
457
474
|
## Contributing
|
|
458
475
|
|