ractor-wrapper 0.1.0 → 0.3.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 +21 -0
- data/LICENSE.md +1 -1
- data/README.md +163 -93
- data/lib/ractor/wrapper/version.rb +3 -1
- data/lib/ractor/wrapper.rb +860 -251
- data/lib/ractor-wrapper.rb +2 -0
- metadata +11 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ceddba72640a063108102f44bf7cf8276305dcfcc4b2807ec24c474997d5dc4
|
|
4
|
+
data.tar.gz: ec46f0323abf7a8468ae8f4b8db82e47d4f6f906ed169d8853f86c82ba50414b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91057acf2a760454fe5864f113f63eadabf54e229b23816784ebdca931be280e85152879ad986ba68100ca49a88d854b856f0af19d69b316a4bcfef09b3d7cdc
|
|
7
|
+
data.tar.gz: 29b1f2b0713bae3737d168578e832d2c4edabf13181ef927dc8473e5b2eee02357eb61c938eccdf1afec5e803af5add40e35c550530d067178e5493cf67abf84
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Release History
|
|
2
2
|
|
|
3
|
+
### v0.3.0 / 2026-01-05
|
|
4
|
+
|
|
5
|
+
This is a major update, and the library, while still experimental, is finally somewhat usable. The examples in the README now actually work!
|
|
6
|
+
|
|
7
|
+
Earlier versions were severely hampered by limitations of the Ractor implementation in Ruby 3. Many of these were fixed in Ruby 4.0, and Ractor::Wrapper has been updated to take advantage of it. This new version requires Ruby 4.0.0 or later, and includes a number of enhancements:
|
|
8
|
+
|
|
9
|
+
* Support for running a wrapper in the current Ractor, useful for wrapping objects that cannot be moved, or that must run in the main Ractor. (By default, wrapped objects are still moved into an isolated Ractor to maximize cleanliness and concurrency.)
|
|
10
|
+
* Support for running a wrapper sequentially without worker threads. This is now the default behavior, which does not spawn any extra threads in the wrapper. (Earlier behavior would spawn exactly one worker thread by default.)
|
|
11
|
+
* Limited support for passing blocks to a wrapped object. You can cause a block to run "in place" within the wrapper, as long as the block can be made shareable (i.e. does not access any outside data), or have the block run in the caller's context with the cost of some additional communication. You can also configure that communication to move or copy data.
|
|
12
|
+
* Provided Ractor::Wrapper#join for waiting for a wrapper to complete without asking for the wrapped object back.
|
|
13
|
+
* Some of the configuration parameters have been renamed.
|
|
14
|
+
|
|
15
|
+
Some caveats remain, so please consult the README for details. This library should still be considered experimental, and not suitable for production use. I reserve the right to make breaking changes at any time.
|
|
16
|
+
|
|
17
|
+
### v0.2.0 / 2021-03-08
|
|
18
|
+
|
|
19
|
+
* BREAKING CHANGE: The wrapper now copies (instead of moves) arguments and return values by default.
|
|
20
|
+
* It is now possible to control, per method, whether arguments and return values are copied or moved.
|
|
21
|
+
* Fixed: The respond_to? method did not work correctly for stubs.
|
|
22
|
+
* Improved: The wrapper server lifecycle is a bit more robust against worker crashes.
|
|
23
|
+
|
|
3
24
|
### v0.1.0 / 2021-03-02
|
|
4
25
|
|
|
5
26
|
* Initial release. HIGHLY EXPERIMENTAL.
|
data/LICENSE.md
CHANGED
data/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# Ractor::Wrapper
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
allowing multiple Ractors to access it concurrently.
|
|
5
|
-
|
|
3
|
+
Ractor::Wrapper is an experimental class that wraps a non-shareable object in
|
|
4
|
+
an actor, allowing multiple Ractors to access it concurrently.
|
|
5
|
+
|
|
6
|
+
**WARNING:** This is a highly experimental library, and currently _not_
|
|
7
|
+
recommended for production use. (As of Ruby 4.0.0, the same can be said of
|
|
8
|
+
Ractors in general.)
|
|
6
9
|
|
|
7
10
|
## Quick start
|
|
8
11
|
|
|
@@ -16,103 +19,170 @@ Require it in your code:
|
|
|
16
19
|
|
|
17
20
|
You can then create wrappers for objects. See the example below.
|
|
18
21
|
|
|
19
|
-
Ractor::Wrapper requires Ruby
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
22
|
+
Ractor::Wrapper requires Ruby 4.0.0 or later.
|
|
23
|
+
|
|
24
|
+
## What is Ractor::Wrapper?
|
|
25
|
+
|
|
26
|
+
For the most part, unless an object is _sharable_, which generally means
|
|
27
|
+
deeply immutable along with a few other restrictions, it cannot be accessed
|
|
28
|
+
directly from another Ractor. This makes it difficult for multiple Ractors
|
|
29
|
+
to share a resource that is stateful. Such a resource must typically itself
|
|
30
|
+
be implemented as a Ractor and accessed via message passing.
|
|
31
|
+
|
|
32
|
+
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
|
+
with an actor that listens for messages and invokes the object's methods in
|
|
35
|
+
a controlled single-Ractor environment. It then provides a stub object that
|
|
36
|
+
reproduces the interface of the original object, but responds to method
|
|
37
|
+
calls by sending messages to the wrapper. Ractor::Wrapper can be used to
|
|
38
|
+
implement simple actors by writing "plain" Ruby objects, or to adapt
|
|
39
|
+
existing non-shareable objects to a multi-Ractor world.
|
|
40
|
+
|
|
41
|
+
### Net::HTTP example
|
|
42
|
+
|
|
43
|
+
The following example shows how to share a single Net::HTTP session object
|
|
44
|
+
among multiple Ractors.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
require "ractor/wrapper"
|
|
48
|
+
require "net/http"
|
|
49
|
+
|
|
50
|
+
# Create a Net::HTTP session. Net::HTTP sessions are not shareable,
|
|
51
|
+
# so normally only one Ractor can access them at a time.
|
|
52
|
+
http = Net::HTTP.new("example.com")
|
|
53
|
+
http.start
|
|
54
|
+
|
|
55
|
+
# Create a wrapper around the session. This moves the session into an
|
|
56
|
+
# internal Ractor and listens for method call requests. By default, a
|
|
57
|
+
# wrapper serializes calls, handling one at a time, for compatibility
|
|
58
|
+
# with non-thread-safe objects.
|
|
59
|
+
wrapper = Ractor::Wrapper.new(http)
|
|
60
|
+
|
|
61
|
+
# At this point, the session object can no longer be accessed directly
|
|
62
|
+
# because it is now owned by the wrapper's internal Ractor.
|
|
63
|
+
# http.get("/whoops") # <= raises Ractor::MovedError
|
|
64
|
+
|
|
65
|
+
# However, you can access the session via the stub object provided by
|
|
66
|
+
# the wrapper. This stub proxies the call to the wrapper's internal
|
|
67
|
+
# Ractor. And it's shareable, so any number of Ractors can use it.
|
|
68
|
+
response = wrapper.stub.get("/")
|
|
69
|
+
|
|
70
|
+
# Here, we start two Ractors, and pass the stub to each one. Each
|
|
71
|
+
# Ractor can simply call methods on the stub as if it were the original
|
|
72
|
+
# connection object. Internally, of course, the calls are proxied to
|
|
73
|
+
# the original object via the wrapper, and execution is serialized.
|
|
74
|
+
r1 = Ractor.new(wrapper.stub) do |stub|
|
|
75
|
+
5.times do
|
|
76
|
+
stub.get("/hello")
|
|
77
|
+
end
|
|
78
|
+
:ok
|
|
79
|
+
end
|
|
80
|
+
r2 = Ractor.new(wrapper.stub) do |stub|
|
|
81
|
+
5.times do
|
|
82
|
+
stub.get("/ruby")
|
|
83
|
+
end
|
|
84
|
+
:ok
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Wait for the two above Ractors to finish.
|
|
88
|
+
r1.join
|
|
89
|
+
r2.join
|
|
90
|
+
|
|
91
|
+
# After you stop the wrapper, you can retrieve the underlying session
|
|
92
|
+
# object and access it directly again.
|
|
93
|
+
wrapper.async_stop
|
|
94
|
+
http = wrapper.recover_object
|
|
95
|
+
http.finish
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### SQLite3 example
|
|
99
|
+
|
|
100
|
+
The following example shows how to share a SQLite3 database among multiple
|
|
101
|
+
Ractors.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
require "ractor/wrapper"
|
|
105
|
+
require "sqlite3"
|
|
106
|
+
|
|
107
|
+
# Create a SQLite3 database. These objects are not shareable, so
|
|
108
|
+
# normally only one Ractor can access them.
|
|
109
|
+
db = SQLite3::Database.new($my_database_path)
|
|
110
|
+
|
|
111
|
+
# Create a wrapper around the database. A SQLite3::Database object
|
|
112
|
+
# cannot be moved between Ractors, so we configure the wrapper to run
|
|
113
|
+
# in the current Ractor. You can also configure it to run multiple
|
|
114
|
+
# worker threads because the database object itself is thread-safe.
|
|
115
|
+
wrapper = Ractor::Wrapper.new(db, use_current_ractor: true, threads: 2)
|
|
116
|
+
|
|
117
|
+
# At this point, the database object can still be accessed directly
|
|
118
|
+
# because it hasn't been moved to a different Ractor.
|
|
119
|
+
rows = db.execute("select * from numbers")
|
|
120
|
+
|
|
121
|
+
# You can also access the database via the stub object provided by the
|
|
122
|
+
# wrapper.
|
|
123
|
+
rows = wrapper.stub.execute("select * from numbers")
|
|
124
|
+
|
|
125
|
+
# Here, we start two Ractors, and pass the stub to each one. The
|
|
126
|
+
# wrapper's two worker threads will handle the requests in the order
|
|
127
|
+
# received.
|
|
128
|
+
r1 = Ractor.new(wrapper.stub) do |stub|
|
|
129
|
+
5.times do
|
|
130
|
+
stub.execute("select * from numbers")
|
|
131
|
+
end
|
|
132
|
+
:ok
|
|
133
|
+
end
|
|
134
|
+
r2 = Ractor.new(wrapper.stub) do |stub|
|
|
135
|
+
5.times do
|
|
136
|
+
stub.execute("select * from numbers")
|
|
137
|
+
end
|
|
138
|
+
:ok
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Wait for the two above Ractors to finish.
|
|
142
|
+
r1.join
|
|
143
|
+
r2.join
|
|
144
|
+
|
|
145
|
+
# After stopping the wrapper, you can call the join method to wait for
|
|
146
|
+
# it to completely finish.
|
|
147
|
+
wrapper.async_stop
|
|
148
|
+
wrapper.join
|
|
149
|
+
|
|
150
|
+
# When running a wrapper with :use_current_ractor, you do not need to
|
|
151
|
+
# recover the object, because it was never moved. The recover_object
|
|
152
|
+
# method is not available.
|
|
153
|
+
# db2 = wrapper.recover_object # <= raises Ractor::Error
|
|
154
|
+
```
|
|
90
155
|
|
|
91
156
|
### Features
|
|
92
157
|
|
|
93
|
-
* Provides a method interface to
|
|
158
|
+
* Provides a Ractor-shareable method interface to a non-shareable object.
|
|
94
159
|
* Supports arbitrary method arguments and return values.
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
|
|
160
|
+
* Can be configured to run in its own isolated Ractor or in a Thread in
|
|
161
|
+
the current Ractor.
|
|
162
|
+
* Can be configured per method whether to copy or move arguments and
|
|
163
|
+
return values.
|
|
164
|
+
* Blocks can be run in the calling Ractor or in the object Ractor.
|
|
165
|
+
* Raises exceptions thrown by the method.
|
|
166
|
+
* Can serialize method calls for non-thread-safe objects, or run methods
|
|
167
|
+
concurrently in multiple worker threads for thread-safe objects.
|
|
98
168
|
* Can gracefully shut down the wrapper and retrieve the original object.
|
|
99
169
|
|
|
100
170
|
### Caveats
|
|
101
171
|
|
|
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
172
|
* Certain types cannot be used as method arguments or return values
|
|
107
|
-
because
|
|
108
|
-
include threads,
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
173
|
+
because they cannot be moved between Ractors. As of Ruby 4.0.0, these
|
|
174
|
+
include threads, backtraces, procs, and a few others.
|
|
175
|
+
* As of Ruby 4.0.0, any exceptions raised are always copied (rather than
|
|
176
|
+
moved) back to the calling Ractor, and the backtrace is cleared out.
|
|
177
|
+
This is due to https://bugs.ruby-lang.org/issues/21818
|
|
178
|
+
* Blocks can be run "in place" (i.e. in the wrapped object context) only
|
|
179
|
+
if the block does not access any data outside the block. Otherwise, the
|
|
180
|
+
block must be run in caller's context.
|
|
181
|
+
* Blocks configured to run in the caller's context can only be run while
|
|
182
|
+
a method is executing. They cannot be "saved" as a proc to be run
|
|
183
|
+
later unless they are configured to run "in place". In particular,
|
|
184
|
+
using blocks as a syntax to define callbacks can generally not be done
|
|
185
|
+
through a wrapper.
|
|
116
186
|
|
|
117
187
|
## Contributing
|
|
118
188
|
|
|
@@ -127,11 +197,11 @@ Development is done in GitHub at https://github.com/dazuma/ractor-wrapper.
|
|
|
127
197
|
|
|
128
198
|
The library uses [toys](https://dazuma.github.io/toys) for testing and CI. To
|
|
129
199
|
run the test suite, `gem install toys` and then run `toys ci`. You can also run
|
|
130
|
-
unit tests, rubocop, and
|
|
200
|
+
unit tests, rubocop, and build tests independently.
|
|
131
201
|
|
|
132
202
|
## License
|
|
133
203
|
|
|
134
|
-
Copyright 2021 Daniel Azuma
|
|
204
|
+
Copyright 2021-2026 Daniel Azuma
|
|
135
205
|
|
|
136
206
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
137
207
|
of this software and associated documentation files (the "Software"), to deal
|