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.
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 in
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 not be
365
- available at all.
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, which itself is
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. Attempting
436
- to `return` within a block that was passed to Ractor::Wrapper will result in an
437
- exception.
438
-
439
- ### Re-entrancy via blocks
440
-
441
- One final known issue with Ractor::Wrapper is that it does not currently handle
442
- re-entrancy resulting from a block making another call to the object. That is,
443
- if a method on a wrapper is called, and it yields to a block that runs back in
444
- the caller's context, and that block then makes another method call to the same
445
- wrapper, now there's a new method call request when the first method is still
446
- being handled (and blocked because it's yielding to the block). Unless the
447
- wrapper is configured with enough threads that another thread can pick up the
448
- new method call, this will deadlock the wrapper: the original method call is
449
- blocked because the yield is not complete, but the yield will never complete
450
- because the new method cannot run until the original method has completed.
451
-
452
- I believe this issue is solvable by retooling the internal method scheduling to
453
- use fibers, and I have filed a to-do item to address it in the future
454
- (https://github.com/dazuma/ractor-wrapper/issues/12). Until then, I do not
455
- recommend making additional calls to a wrapper from within a yielded block.
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
 
@@ -7,6 +7,6 @@ class Ractor
7
7
  #
8
8
  # @return [String]
9
9
  #
10
- VERSION = "0.4.0"
10
+ VERSION = "0.5.0"
11
11
  end
12
12
  end