ractor-wrapper 0.3.0 → 0.4.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/CHANGELOG.md +17 -0
- data/CLAUDE.md +76 -0
- data/README.md +322 -52
- data/lib/ractor/wrapper/version.rb +1 -1
- data/lib/ractor/wrapper.rb +497 -248
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3948e5f98b34503dc8e6075eb8a186ae1c76a53f0f45164396bb006ad802964
|
|
4
|
+
data.tar.gz: 58eaa1ac8d9d0088b1f8df5e45d746a084a921eef53eb96563c0700eb9144dde
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b31f168446571aad39874d04542818ccea56bb3165e2a081fb5135f72b0498c2e73eb5fec4431cd712f8326ced6c6924a3600833895e356fb7cb154ac6f2023
|
|
7
|
+
data.tar.gz: 9d679b50a36ee4303117d46bd404f649ca1a71f724c8dbe6ba13ba28f77228e1071839d7b06d8c965cb7946e411178b86bcfb08b77c0bfdc2e0c2420121ede1a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Release History
|
|
2
2
|
|
|
3
|
+
### v0.4.0 / 2026-03-30
|
|
4
|
+
|
|
5
|
+
This release includes two major changes: it greatly improves robustness in the case of server crashes, and it reworks the method call configuration interface. This involves several breaking changes, and I expect the interface will continue to be a bit unstable for now as I'm working through use cases and edge cases. The README has also been expanded to include more information on the configuration options and the known issues.
|
|
6
|
+
|
|
7
|
+
* ADDED: Uses a separate `Ractor::Wrapper::Configuration` class for block-based initialization. Removed the configuration mutation methods from `Ractor::Wrapper` itself.
|
|
8
|
+
* BREAKING CHANGE: The method configuration interface now uses symbolic settings values instead of booleans for more flexibility
|
|
9
|
+
* ADDED: Support for suppressing return values for methods and blocks that unintentionally return something they shouldn't
|
|
10
|
+
* BREAKING CHANGE: Raises `Ractor::Wrapper::StoppedError` instead of `Ractor::ClosedError` if a method is called via the wrapper after the wrapper has stopped
|
|
11
|
+
* BREAKING CHANGE: `Wrapper#join` now returns normally rather than raising, if an isolated wrapper terminated due to a crash
|
|
12
|
+
* BREAKING FIX: `Wrapper#join` no longer hangs if a local wrapper crashes, but returns to indicate that the wrapper has stopped (albeit non-normally)
|
|
13
|
+
* FIXED: Internal cleanup is more robust if a crash occurs in the wrapper
|
|
14
|
+
* FIXED: Method calls raise `Ractor::Wrapper::CrashedError` instead of hanging if the wrapper crashes during handling
|
|
15
|
+
* FIXED: Prevented port leaks if a method call send or a block yield send fails
|
|
16
|
+
* FIXED: Methods that return or yield self return/yield the stub instead
|
|
17
|
+
* FIXED: The `recover_object` method now raises `Ractor::Wrapper::Error` if recovery failed
|
|
18
|
+
* DOCS: Updates to README
|
|
19
|
+
|
|
3
20
|
### v0.3.0 / 2026-01-05
|
|
4
21
|
|
|
5
22
|
This is a major update, and the library, while still experimental, is finally somewhat usable. The examples in the README now actually work!
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
This project uses [Toys](https://dazuma.github.io/toys) for task management (not Rake).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Run full CI pipeline
|
|
11
|
+
toys ci
|
|
12
|
+
|
|
13
|
+
# Run individual components
|
|
14
|
+
toys test # Tests only
|
|
15
|
+
toys rubocop # Linting and code style only
|
|
16
|
+
toys yardoc # Documentation only
|
|
17
|
+
toys build # Build gem only
|
|
18
|
+
|
|
19
|
+
# Run a single test file
|
|
20
|
+
toys test test/test_wrapper.rb
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
The entire library lives in `lib/ractor/wrapper.rb`. The public entry point is `Ractor::Wrapper`, which wraps a non-shareable object and exposes it to other Ractors via a shareable `Stub` proxy.
|
|
26
|
+
|
|
27
|
+
### Core classes
|
|
28
|
+
|
|
29
|
+
- **`Ractor::Wrapper`** — Public API. Wraps an object and manages its lifecycle. Accepts options like `use_current_ractor:`, `threads:`, `name:`, and per-method settings via `configure_method`.
|
|
30
|
+
- **`Ractor::Wrapper::Stub`** — Frozen, shareable proxy passed to other Ractors. Uses `method_missing` to forward calls back to the wrapper via message passing.
|
|
31
|
+
- **`Ractor::Wrapper::MethodSettings`** — Frozen configuration controlling copy vs. move semantics for arguments and return values, and block handling behavior.
|
|
32
|
+
- **`Ractor::Wrapper::Server`** — Private backend. Receives `CallMessage` objects and dispatches them to the wrapped object, then returns results via `ReturnMessage`, `ExceptionMessage`, or `YieldMessage`.
|
|
33
|
+
|
|
34
|
+
### Two execution modes
|
|
35
|
+
|
|
36
|
+
1. **Isolated mode** (default) — the wrapped object is moved into a new Ractor. Other Ractors interact with it through the Stub. After `join`, the object can be recovered via `recover_object`.
|
|
37
|
+
2. **Local mode** (`use_current_ractor: true`) — the server runs as Thread(s) inside the current Ractor. The object is never moved. Used for objects that cannot be transferred between Ractors (e.g., SQLite3 connections).
|
|
38
|
+
|
|
39
|
+
### Concurrency within the server
|
|
40
|
+
|
|
41
|
+
- **Sequential** (default) — one call at a time, no worker threads.
|
|
42
|
+
- **Concurrent** — multiple worker threads (set via `threads:`), for thread-safe wrapped objects.
|
|
43
|
+
|
|
44
|
+
### Message protocol
|
|
45
|
+
|
|
46
|
+
All inter-Ractor communication uses frozen message structs defined in the file: `CallMessage`, `ReturnMessage`, `ExceptionMessage`, `YieldMessage`, `StopMessage`, `JoinMessage`, `WorkerStoppedMessage`. Block calls round-trip: the server sends a `YieldMessage` to the caller Ractor, the caller executes the block and sends back a result or exception.
|
|
47
|
+
|
|
48
|
+
### Lifecycle
|
|
49
|
+
|
|
50
|
+
1. Wrapper starts → Server enters **running** phase (accepts calls).
|
|
51
|
+
2. `async_stop` or `stop` called → Server enters **stopping** phase (rejects new calls, drains workers).
|
|
52
|
+
3. All workers finish → Server enters **cleanup** phase and shuts down.
|
|
53
|
+
4. `join` returns → In isolated mode, `recover_object` retrieves the wrapped object.
|
|
54
|
+
|
|
55
|
+
## Code Style
|
|
56
|
+
|
|
57
|
+
- Ruby 4.0+ target
|
|
58
|
+
- Double-quoted strings (`Style/StringLiterals: double_quotes`)
|
|
59
|
+
- Trailing commas in multiline arrays and hashes
|
|
60
|
+
- Bracket-style symbol and word arrays (`[:foo, :bar]` not `%i[foo bar]`)
|
|
61
|
+
- Max line length: 120
|
|
62
|
+
- `Style/DocumentationMethod: Enabled` — public methods require YARD docs
|
|
63
|
+
- Tests use Minitest spec style with assertions (not expectations)
|
|
64
|
+
- Top-level constants must be prefixed with `::` (e.g. `::File`, `::Regexp`, `::Gem::Version`) to avoid ambiguous resolution within nested namespaces. Relative constants defined within the current namespace should not be prefixed. Note that Kernel method calls such as `Array(x)`, `Integer(x)`, `Float(x)` look like constants but are not and do not get the prefix.
|
|
65
|
+
|
|
66
|
+
## Testing
|
|
67
|
+
|
|
68
|
+
- Minitest spec style: `describe`/`it` blocks with `assert_*` assertions (not expectations)
|
|
69
|
+
- Test files follow the `test_*.rb` naming convention
|
|
70
|
+
|
|
71
|
+
## General coding instructions
|
|
72
|
+
|
|
73
|
+
- Unless instructed otherwise, always use red-green test-driven development when making code changes. For each step in a coding task, first write tests and confirm they fail. Then write code to make the tests pass.
|
|
74
|
+
- Unless instructed otherwise, always git commit after a step is complete and the tests pass.
|
|
75
|
+
- Conventional Commits format required (`fix:`, `feat:`, `docs:`, etc.)
|
|
76
|
+
- Prefer Ruby for any one-off scripts you need to write as part of your work.
|
data/README.md
CHANGED
|
@@ -3,19 +3,23 @@
|
|
|
3
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
|
-
**WARNING:** This is
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
**WARNING:** This is an experimental library, and currently _not_ recommended
|
|
7
|
+
for production use. (As of Ruby 4.0, the same can still be said of Ractors in
|
|
8
|
+
general.)
|
|
9
9
|
|
|
10
10
|
## Quick start
|
|
11
11
|
|
|
12
12
|
Install ractor-wrapper as a gem, or include it in your bundle.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
```sh
|
|
15
|
+
gem install ractor-wrapper
|
|
16
|
+
```
|
|
15
17
|
|
|
16
18
|
Require it in your code:
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
```ruby
|
|
21
|
+
require "ractor/wrapper"
|
|
22
|
+
```
|
|
19
23
|
|
|
20
24
|
You can then create wrappers for objects. See the example below.
|
|
21
25
|
|
|
@@ -23,20 +27,52 @@ Ractor::Wrapper requires Ruby 4.0.0 or later.
|
|
|
23
27
|
|
|
24
28
|
## What is Ractor::Wrapper?
|
|
25
29
|
|
|
26
|
-
For the most part, unless an object is
|
|
30
|
+
For the most part, unless an object is _shareable_, which generally means
|
|
27
31
|
deeply immutable along with a few other restrictions, it cannot be accessed
|
|
28
|
-
directly from
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
directly from a Ractor other than the one in which it was constructed. This
|
|
33
|
+
makes it difficult for multiple Ractors to share a resource that is stateful,
|
|
34
|
+
such as a database connection.
|
|
35
|
+
|
|
36
|
+
+----Main-Ractor----+ +-Another-Ractor-+
|
|
37
|
+
| | | |
|
|
38
|
+
| client1 | | |
|
|
39
|
+
| | | | |
|
|
40
|
+
| | ok | | |
|
|
41
|
+
| v | | |
|
|
42
|
+
| my_db_conn <------X------ client2 |
|
|
43
|
+
| | fails | |
|
|
44
|
+
+-------------------+ +----------------+
|
|
31
45
|
|
|
32
46
|
Ractor::Wrapper makes it possible for an ordinary non-shareable object to
|
|
33
|
-
be accessed from multiple Ractors. It does this by "wrapping" the object
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
be accessed from multiple Ractors. It does this by "wrapping" the object with
|
|
48
|
+
a shareable proxy.
|
|
49
|
+
|
|
50
|
+
+--Main-Ractor--+ +-Wrapper-Ractor-+ +-Another-Ractor-+
|
|
51
|
+
| | | | | |
|
|
52
|
+
| client1 | | | | client2 |
|
|
53
|
+
| | | | | | | |
|
|
54
|
+
| v | | | | v |
|
|
55
|
+
| +----------------------------------------------+ |
|
|
56
|
+
| | SHAREABLE WRAPPER | |
|
|
57
|
+
| +----------------------------------------------+ |
|
|
58
|
+
| | | | | | |
|
|
59
|
+
| | | v | | |
|
|
60
|
+
| | | my_db_conn | | |
|
|
61
|
+
+---------------+ +----------------+ +----------------+
|
|
62
|
+
|
|
63
|
+
The wrapper provides a shareable stub object that reproduces the method
|
|
64
|
+
interface of the original object, so, with a few caveats, the wrapper is almost
|
|
65
|
+
fully transparent. Behind the scenes, the wrapper "runs" the wrapped object in
|
|
66
|
+
a controlled single-Ractor environment, and uses port messaging to communicate
|
|
67
|
+
method calls, arguments, and return values between Ractors.
|
|
68
|
+
|
|
69
|
+
Ractor::Wrapper can be used to adapt non-shareable objects to a multi-Ractor
|
|
70
|
+
world. It can also be used to implement a simple actor by writing a "plain"
|
|
71
|
+
Ruby object and wrapping it with a Ractor.
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
Below are some illustrative examples showing how to use Ractor::Wrapper.
|
|
40
76
|
|
|
41
77
|
### Net::HTTP example
|
|
42
78
|
|
|
@@ -44,6 +80,8 @@ The following example shows how to share a single Net::HTTP session object
|
|
|
44
80
|
among multiple Ractors.
|
|
45
81
|
|
|
46
82
|
```ruby
|
|
83
|
+
# Net::HTTP example
|
|
84
|
+
|
|
47
85
|
require "ractor/wrapper"
|
|
48
86
|
require "net/http"
|
|
49
87
|
|
|
@@ -101,6 +139,8 @@ The following example shows how to share a SQLite3 database among multiple
|
|
|
101
139
|
Ractors.
|
|
102
140
|
|
|
103
141
|
```ruby
|
|
142
|
+
# SQLite3 example
|
|
143
|
+
|
|
104
144
|
require "ractor/wrapper"
|
|
105
145
|
require "sqlite3"
|
|
106
146
|
|
|
@@ -110,12 +150,13 @@ db = SQLite3::Database.new($my_database_path)
|
|
|
110
150
|
|
|
111
151
|
# Create a wrapper around the database. A SQLite3::Database object
|
|
112
152
|
# cannot be moved between Ractors, so we configure the wrapper to run
|
|
113
|
-
# in the current Ractor
|
|
114
|
-
# worker threads because the database
|
|
153
|
+
# in the current Ractor instead of an internal Ractor. We can also
|
|
154
|
+
# configure it to run multiple worker threads because the database
|
|
155
|
+
# object itself is thread-safe.
|
|
115
156
|
wrapper = Ractor::Wrapper.new(db, use_current_ractor: true, threads: 2)
|
|
116
157
|
|
|
117
158
|
# At this point, the database object can still be accessed directly
|
|
118
|
-
# because it hasn't been moved
|
|
159
|
+
# from the current Ractor because it hasn't been moved.
|
|
119
160
|
rows = db.execute("select * from numbers")
|
|
120
161
|
|
|
121
162
|
# You can also access the database via the stub object provided by the
|
|
@@ -123,8 +164,7 @@ rows = db.execute("select * from numbers")
|
|
|
123
164
|
rows = wrapper.stub.execute("select * from numbers")
|
|
124
165
|
|
|
125
166
|
# Here, we start two Ractors, and pass the stub to each one. The
|
|
126
|
-
# wrapper's
|
|
127
|
-
# received.
|
|
167
|
+
# wrapper's worker threads will handle the requests concurrently.
|
|
128
168
|
r1 = Ractor.new(wrapper.stub) do |stub|
|
|
129
169
|
5.times do
|
|
130
170
|
stub.execute("select * from numbers")
|
|
@@ -150,39 +190,269 @@ wrapper.join
|
|
|
150
190
|
# When running a wrapper with :use_current_ractor, you do not need to
|
|
151
191
|
# recover the object, because it was never moved. The recover_object
|
|
152
192
|
# method is not available.
|
|
153
|
-
# db2 = wrapper.recover_object # <= raises Ractor::Error
|
|
193
|
+
# db2 = wrapper.recover_object # <= raises Ractor::Wrapper::Error
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Simple actor example
|
|
197
|
+
|
|
198
|
+
The following example demonstrates how to use Ractor::Wrapper to implement an
|
|
199
|
+
actor as a plain Ruby object. Focus on writing functionality as methods, and
|
|
200
|
+
let Ractor::Wrapper handle all the messaging logic.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# Simple actor example
|
|
204
|
+
|
|
205
|
+
require "ractor/wrapper"
|
|
206
|
+
|
|
207
|
+
class SimpleCalculator
|
|
208
|
+
class EmptyStackError < StandardError
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def initialize
|
|
212
|
+
@stack = []
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def push(number)
|
|
216
|
+
@stack.push(number)
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def pop
|
|
221
|
+
raise EmptyStackError if @stack.empty?
|
|
222
|
+
@stack.pop
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def add
|
|
226
|
+
push(pop + pop)
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Create an actor based on SimpleCalculator
|
|
232
|
+
calc_actor = Ractor::Wrapper.new(SimpleCalculator.new)
|
|
233
|
+
|
|
234
|
+
# You can now send messages by calling methods
|
|
235
|
+
calc_stub = calc_actor.stub
|
|
236
|
+
calc_stub.push(2)
|
|
237
|
+
calc_stub.push(3)
|
|
238
|
+
calc_stub.add
|
|
239
|
+
sum = calc_stub.pop
|
|
240
|
+
|
|
241
|
+
# Stop the actor by calling async_stop
|
|
242
|
+
calc_actor.async_stop
|
|
243
|
+
# Wait for the actor to shut down
|
|
244
|
+
calc_actor.join
|
|
154
245
|
```
|
|
155
246
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
247
|
+
## Configuring a wrapper
|
|
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.
|
|
252
|
+
Alternatively, you can pass a block to the constructor; the constructor will
|
|
253
|
+
yield a configuration interface to your block, letting you configure the
|
|
254
|
+
wrapper's behavior in detail.
|
|
255
|
+
|
|
256
|
+
The various configuration options are described below.
|
|
257
|
+
|
|
258
|
+
### Current Ractor mode
|
|
259
|
+
|
|
260
|
+
Normally a wrapper will spawn a new Ractor and move the wrapped object into
|
|
261
|
+
that Ractor. We call this default mode the "isolated Ractor" mode. Isolated
|
|
262
|
+
Ractor lets the object function as an actor that can be called uniformly from
|
|
263
|
+
any Ractor.
|
|
264
|
+
|
|
265
|
+
However, some objects cannot be moved to a different Ractor. This in particular
|
|
266
|
+
can include certain C-based I/O objects such as database connections.
|
|
267
|
+
Additionally, there are other objects that can live only in the main Ractor. If
|
|
268
|
+
the object to be wrapped cannot be moved to its own Ractor, configure it with
|
|
269
|
+
`use_current_ractor`, which will run the wrapper in a Thread in the calling
|
|
270
|
+
Ractor rather than trying to move it to its own Ractor. The SQLite3 example
|
|
271
|
+
above demonstrates wrapping an object that cannot be moved to its own Ractor.
|
|
272
|
+
|
|
273
|
+
### Sequential vs concurrent execution
|
|
274
|
+
|
|
275
|
+
By default, wrappers run sequentially in a single Thread. The wrapper will
|
|
276
|
+
handle only a single method call at a time, and any other concurrent requests
|
|
277
|
+
are queued and blocked until their turn. This is the behavior of the classic
|
|
278
|
+
actor model, and in particular is appropriate for wrapped objects that are not
|
|
279
|
+
thread-safe.
|
|
280
|
+
|
|
281
|
+
You can, however, configure a wrapper with concurrent access. This will spin up
|
|
282
|
+
a configurable number of worker threads within the wrapper, to handle
|
|
283
|
+
potentially concurrent method calls. You should set this configuration only if
|
|
284
|
+
you are certain the wrapped object can handle concurrent access.
|
|
285
|
+
|
|
286
|
+
### Data communication options
|
|
287
|
+
|
|
288
|
+
When you call a method on a wrapper, and you pass arguments and receive a
|
|
289
|
+
return value, or you pass a block that can receive arguments and return a
|
|
290
|
+
value, those objects are communicated to and from the wrapper via Ractor ports.
|
|
291
|
+
As such, if they are not shareable, they may be *copied* or *moved*. By
|
|
292
|
+
default, values are copied in order to minimize interference with surrounding
|
|
293
|
+
code, but a wrapper can be configured to move objects instead.
|
|
294
|
+
|
|
295
|
+
This configuration is done per-method, using the `configure_method` call in the
|
|
296
|
+
configuration block. You can, for particular method names, specify whether each
|
|
297
|
+
type of value: arguments, return values, block arguments, and block return
|
|
298
|
+
values, are copied or moved. For any given method, you must configure all
|
|
299
|
+
arguments to be handled the same way, but different methods can have different
|
|
300
|
+
configurations. You can also provide a default configuration that will apply to
|
|
301
|
+
all method names that are not explicitly configured.
|
|
302
|
+
|
|
303
|
+
Return values (and block return values) have a third configuration option:
|
|
304
|
+
*void*. This option disables communication of return values, sending `nil`
|
|
305
|
+
instead of what was actually returned from the method. This is intended for
|
|
306
|
+
methods that do not *semantically* need to return anything, but because of
|
|
307
|
+
their implementation they actually do return some internal object. You can use
|
|
308
|
+
the *void* option to prevent those methods from wasting resources copying a
|
|
309
|
+
return object unnecessarily, or worse, moving an object that shouldn't be moved.
|
|
310
|
+
|
|
311
|
+
### Block execution environment
|
|
312
|
+
|
|
313
|
+
If a block is passed to a method, it is handled in one of two ways. By default,
|
|
314
|
+
if/when the method yields to the block, the wrapper will send a message *back*
|
|
315
|
+
to the caller, and the block will be executed in the caller's environment. In
|
|
316
|
+
most cases, this is what you want; your block may access information from its
|
|
317
|
+
lexical environment, and that environment would not be available to the wrapped
|
|
318
|
+
object. However, this extra communication can add overhead.
|
|
319
|
+
|
|
320
|
+
As an alternative, you can configure, per-method, blocks to be executed in the
|
|
321
|
+
context of the *wrapped object*. Effectively, the block itself is *moved* into
|
|
322
|
+
the wrapped object's Ractor/context, and called directly. This will work only
|
|
323
|
+
if the block does not access any information from its lexical context, or
|
|
324
|
+
anything that cannot be accessed from a different Ractor. A block must truly be
|
|
325
|
+
self-contained in order to use this option.
|
|
326
|
+
|
|
327
|
+
As with data communication options, configuring block execution environment is
|
|
328
|
+
done using the `configure_method` call in the configuration block. You can set
|
|
329
|
+
the environment either to `:caller` or `:wrapped`, and you can do so for an
|
|
330
|
+
individual method or provide a default to apply to all methods not explicitly
|
|
331
|
+
configured.
|
|
332
|
+
|
|
333
|
+
## Additional features
|
|
334
|
+
|
|
335
|
+
### Wrapper shutdown
|
|
336
|
+
|
|
337
|
+
If you are done with a wrapper, you should shut it down by calling `async_stop`.
|
|
338
|
+
This method will initiate a graceful shutdown of the wrapper, finishing any
|
|
339
|
+
pending method calls, and putting the wrapper in a state where it will refuse
|
|
340
|
+
new calls. Any additional method calls will cause a
|
|
341
|
+
`Ractor::Wrapper::StoppedError` to be raised.
|
|
342
|
+
|
|
343
|
+
Ractor::Wrapper also provides a `join` method that can be called to wait for
|
|
344
|
+
the wrapper to complete its shutdown.
|
|
345
|
+
|
|
346
|
+
### Wrapped object access
|
|
347
|
+
|
|
348
|
+
The general intent is that once you've wrapped an object, all access should go
|
|
349
|
+
through the wrapper. In the default "isolated Ractor" mode, the wrapped object
|
|
350
|
+
is in fact *moved* to a different Ractor, so the Ractor system will prevent you
|
|
351
|
+
from accessing it directly. In "current Ractor" mode, the wrapped object is not
|
|
352
|
+
moved, so you technically could continue to access it directly from its
|
|
353
|
+
original Ractor. But beware: the wrapper runs a thread and will be making calls
|
|
354
|
+
to the object from that thread, which may cause you problems if the object is
|
|
355
|
+
not thread-safe.
|
|
356
|
+
|
|
357
|
+
In "isolated Ractor" mode, after you shut down the wrapper, you can recover the
|
|
358
|
+
original object by calling `recover_object`. Only one Ractor can call this
|
|
359
|
+
method; the object will be moved into the requesting Ractor, and any other
|
|
360
|
+
Ractor that subsequently requests the object will get an exception instead.
|
|
361
|
+
|
|
362
|
+
In "current Ractor" mode, the object will never have been moved to a different
|
|
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.
|
|
366
|
+
|
|
367
|
+
### Error handling
|
|
368
|
+
|
|
369
|
+
Ractor::Wrapper provides fairly robust handling of errors. If a method call
|
|
370
|
+
raises an exception, the exception will be passed back to the caller and raised
|
|
371
|
+
there. In the unlikely event that the wrapper itself crashes, it goes through a
|
|
372
|
+
very thorough clean-up process and makes every effort to shut down gracefully,
|
|
373
|
+
notifying any pending method calls that the wrapper has crashed by raising
|
|
374
|
+
`Ractor::Wrapper::CrashedError`.
|
|
375
|
+
|
|
376
|
+
### Automatic stub conversion
|
|
377
|
+
|
|
378
|
+
One special case handled by the wrapper is methods that return `self`. This is
|
|
379
|
+
a common pattern in Ruby and is used to allow "chaining" interfaces. However,
|
|
380
|
+
you generally cannot return `self` from a wrapped object because, depending on
|
|
381
|
+
the communication configuration, you'll either get a *copy* of `self`, or
|
|
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
|
|
384
|
+
replaces it with the wrapper's stub object. The stub is shareable, and designed
|
|
385
|
+
to have the same usage as the original object, so this should work for most use
|
|
386
|
+
cases.
|
|
387
|
+
|
|
388
|
+
## Known issues
|
|
389
|
+
|
|
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
|
|
392
|
+
essentially a workaround to the fact that Ruby has a lot of use cases that
|
|
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.
|
|
395
|
+
|
|
396
|
+
### Data communication issues
|
|
397
|
+
|
|
398
|
+
As of Ruby 4.0, most objects have been retrofitted to play reasonably with
|
|
399
|
+
Ractors. Some objects are shareable across Ractors, and most others can be
|
|
400
|
+
moved from one Ractor to another. However, there are a few objects that,
|
|
401
|
+
because of their semantics or details about their implementation, cannot be
|
|
402
|
+
moved and are confined to their creating Ractor (or in some cases, only the
|
|
403
|
+
main Ractor.) These may include objects such as threads, procs, backtraces, and
|
|
404
|
+
certain C-based objects.
|
|
405
|
+
|
|
406
|
+
One particular case of note is exception objects, which one might expect to be
|
|
407
|
+
shareable, but are not. Furthermore, they cannot be moved, and even copying an
|
|
408
|
+
exception has issues (in particular the backtrace of a copy gets cleared out).
|
|
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
|
|
411
|
+
means that currently backtraces will not be present.
|
|
412
|
+
|
|
413
|
+
### Blocks
|
|
414
|
+
|
|
415
|
+
Ruby blocks pose particular challenges for Ractor::Wrapper because of their
|
|
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
|
|
418
|
+
wrapped object's context, which may limit what the block can do. Additionally,
|
|
419
|
+
the following restrictions apply to blocks:
|
|
420
|
+
|
|
421
|
+
Blocks configured to run in the caller's context can be run only while the
|
|
422
|
+
method is executing; i.e. they can only be "yielded" to. The wrapped object
|
|
423
|
+
cannot "save" the block as a proc to be run later, unless the block is
|
|
424
|
+
configured to run in the "wrapped object's" context. This is simply because we
|
|
425
|
+
have access to the caller only while the caller is making a method call. After
|
|
426
|
+
the call is done, we no longer have access to that context, and there's no
|
|
427
|
+
guarantee that the caller or its Ractor even exists anymore. In particular,
|
|
428
|
+
this means that the common Ruby idiom of using blocks to define callbacks (that
|
|
429
|
+
run in the context of the code defining the callback) can generally not be done
|
|
430
|
+
through a wrapper.
|
|
431
|
+
|
|
432
|
+
In Ruby, it is legal (although not considered very good practice) to do a
|
|
433
|
+
non-local `return` from inside a block. Assuming the block isn't being defined
|
|
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.
|
|
186
456
|
|
|
187
457
|
## Contributing
|
|
188
458
|
|