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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba8fea27b45cfb0ee50c95d09c19c43e3aa185db4871e1e5f892551de35c33eb
4
- data.tar.gz: ae2558e2e07b3bc74c2409c10f91a997b391709c8d1ee10157327684f4528bc5
3
+ metadata.gz: 0ceddba72640a063108102f44bf7cf8276305dcfcc4b2807ec24c474997d5dc4
4
+ data.tar.gz: ec46f0323abf7a8468ae8f4b8db82e47d4f6f906ed169d8853f86c82ba50414b
5
5
  SHA512:
6
- metadata.gz: 0e76769526ea1db2f95819275b87252a29c188aaad1a1caf5d610d84b9e1cbe18344f4266c96e2274a2ff972c6faf910393be811dfa3496c0a715aeda415f7d4
7
- data.tar.gz: ac39e5fc611ad01bedfaf2f4c065108d9d487dfd040a21ed26845d1d92968cd137aed98c96cb5a7e45ec3485e6da03d5338685521799edf221f0f836557495e5
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
@@ -1,6 +1,6 @@
1
1
  # License
2
2
 
3
- Copyright 2021 Daniel Azuma
3
+ Copyright 2021-2026 Daniel Azuma
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # Ractor::Wrapper
2
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.
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 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")
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 an object running in a different Ractor.
158
+ * Provides a Ractor-shareable method interface to a non-shareable object.
94
159
  * 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.
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 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)
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 builds independently.
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Ractor
2
4
  class Wrapper
3
5
  ##
@@ -5,6 +7,6 @@ class Ractor
5
7
  #
6
8
  # @return [String]
7
9
  #
8
- VERSION = "0.1.0".freeze
10
+ VERSION = "0.3.0"
9
11
  end
10
12
  end