ractor-wrapper 0.2.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: 9fec3af2b1b8b9c105260fe2fca50d69e48de205dd2dd791592317ee41286af3
4
- data.tar.gz: e7b4487502427ec05f3dc530925e9efecd8d599e75f4dc2b82c7371592986f59
3
+ metadata.gz: 0ceddba72640a063108102f44bf7cf8276305dcfcc4b2807ec24c474997d5dc4
4
+ data.tar.gz: ec46f0323abf7a8468ae8f4b8db82e47d4f6f906ed169d8853f86c82ba50414b
5
5
  SHA512:
6
- metadata.gz: 0ab154c16e2ed53f65a042bf18b85448cb0115e6b1616db5cce96084767ae1f00c23392ba48560177f34c43d757b59e282c05891702af927f40f72fe989b33e1
7
- data.tar.gz: 8e16f5694e46c571deac8d66f56bb23467572bad838038d941955f2edaca76537ef741df79b63da0bc28c31708a31ac3ba699e3a2a661b56552dc6b1050625a0
6
+ metadata.gz: 91057acf2a760454fe5864f113f63eadabf54e229b23816784ebdca931be280e85152879ad986ba68100ca49a88d854b856f0af19d69b316a4bcfef09b3d7cdc
7
+ data.tar.gz: 29b1f2b0713bae3737d168578e832d2c4edabf13181ef927dc8473e5b2eee02357eb61c938eccdf1afec5e803af5add40e35c550530d067178e5493cf67abf84
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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
+
3
17
  ### v0.2.0 / 2021-03-08
4
18
 
5
19
  * BREAKING CHANGE: The wrapper now copies (instead of moves) arguments and return values by default.
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,108 +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.
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.
20
31
 
21
- WARNING: This is a highly experimental library, and currently _not_ recommended
22
- for production use. (As of Ruby 3.0.0, the same can be said of Ractors in
23
- general.)
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.
24
40
 
25
- ## About Ractor::Wrapper
41
+ ### Net::HTTP example
26
42
 
27
- Ractors for the most part cannot access objects concurrently with other
28
- Ractors unless the object is _shareable_ (that is, deeply immutable along
29
- with a few other restrictions.) If multiple Ractors need to interact with a
30
- shared resource that is stateful or otherwise not Ractor-shareable, that
31
- resource must itself be implemented and accessed as a Ractor.
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
32
86
 
33
- `Ractor::Wrapper` makes it possible for such a shared resource to be
34
- implemented as an ordinary object and accessed using ordinary method calls. It
35
- does this by "wrapping" the object in a Ractor, and mapping method calls to
36
- message passing. This may make it easier to implement such a resource with
37
- a simple class rather than a full-blown Ractor with message passing, and it
38
- may also useful for adapting existing legacy object-based implementations.
87
+ # Wait for the two above Ractors to finish.
88
+ r1.join
89
+ r2.join
39
90
 
40
- Given a shared resource object, `Ractor::Wrapper` starts a new Ractor and
41
- "runs" the object within that Ractor. It provides you with a stub object
42
- on which you can invoke methods. The wrapper responds to these method calls
43
- by sending messages to the internal Ractor, which invokes the shared object
44
- and then sends back the result. If the underlying object is thread-safe,
45
- you can configure the wrapper to run multiple threads that can run methods
46
- concurrently. Or, if not, the wrapper can serialize requests to the object.
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
+ ```
47
97
 
48
- ### Example usage
98
+ ### SQLite3 example
49
99
 
50
- The following example shows how to share a single `Faraday::Conection`
51
- object among multiple Ractors. Because `Faraday::Connection` is not itself
52
- thread-safe, this example serializes all calls to it.
100
+ The following example shows how to share a SQLite3 database among multiple
101
+ Ractors.
53
102
 
54
103
  ```ruby
55
- require "faraday"
56
104
  require "ractor/wrapper"
57
-
58
- # Create a Faraday connection and a wrapper for it.
59
- connection = Faraday.new "http://example.com"
60
- wrapper = Ractor::Wrapper.new(connection)
61
-
62
- # At this point, the connection object cannot be accessed directly
63
- # because it has been "moved" to the wrapper's internal Ractor.
64
- # connection.get("/whoops") # <= raises an error
65
-
66
- # However, any number of Ractors can now access it through the wrapper.
67
- # By default, access to the object is serialized; methods will not be
68
- # invoked concurrently. (To allow concurrent access, set up threads when
69
- # creating the wrapper.)
70
- r1 = Ractor.new(wrapper) do |w|
71
- 10.times do
72
- w.stub.get("/hello")
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")
73
131
  end
74
132
  :ok
75
133
  end
76
- r2 = Ractor.new(wrapper) do |w|
77
- 10.times do
78
- w.stub.get("/ruby")
134
+ r2 = Ractor.new(wrapper.stub) do |stub|
135
+ 5.times do
136
+ stub.execute("select * from numbers")
79
137
  end
80
138
  :ok
81
139
  end
82
140
 
83
141
  # Wait for the two above Ractors to finish.
84
- r1.take
85
- r2.take
142
+ r1.join
143
+ r2.join
86
144
 
87
- # After you stop the wrapper, you can retrieve the underlying
88
- # connection object and access it directly again.
145
+ # After stopping the wrapper, you can call the join method to wait for
146
+ # it to completely finish.
89
147
  wrapper.async_stop
90
- connection = wrapper.recover_object
91
- connection.get("/finally")
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
92
154
  ```
93
155
 
94
156
  ### Features
95
157
 
96
- * Provides a method interface to an object running in a different Ractor.
158
+ * Provides a Ractor-shareable method interface to a non-shareable object.
97
159
  * Supports arbitrary method arguments and return values.
98
- * Supports exceptions thrown by the method.
99
- * Can be configured to copy or move arguments, return values, and
100
- exceptions, per method.
101
- * Can serialize method calls for non-concurrency-safe objects, or run
102
- 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.
103
168
  * Can gracefully shut down the wrapper and retrieve the original object.
104
169
 
105
170
  ### Caveats
106
171
 
107
- Ractor::Wrapper is subject to some limitations (and bugs) of Ractors, as of
108
- Ruby 3.0.0.
109
-
110
- * You cannot pass blocks to wrapped methods.
111
172
  * Certain types cannot be used as method arguments or return values
112
- because Ractor does not allow them to be moved between Ractors. These
113
- include threads, procs, backtraces, and a few others.
114
- * You can call wrapper methods from multiple Ractors concurrently, but
115
- you cannot call them from multiple Threads within a single Ractor.
116
- (This is due to https://bugs.ruby-lang.org/issues/17624)
117
- * If you close the incoming port on a Ractor, it will no longer be able
118
- to call out via a wrapper. If you close its incoming port while a call
119
- is currently pending, that call may hang. (This is due to
120
- 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.
121
186
 
122
187
  ## Contributing
123
188
 
@@ -132,11 +197,11 @@ Development is done in GitHub at https://github.com/dazuma/ractor-wrapper.
132
197
 
133
198
  The library uses [toys](https://dazuma.github.io/toys) for testing and CI. To
134
199
  run the test suite, `gem install toys` and then run `toys ci`. You can also run
135
- unit tests, rubocop, and builds independently.
200
+ unit tests, rubocop, and build tests independently.
136
201
 
137
202
  ## License
138
203
 
139
- Copyright 2021 Daniel Azuma
204
+ Copyright 2021-2026 Daniel Azuma
140
205
 
141
206
  Permission is hereby granted, free of charge, to any person obtaining a copy
142
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.2.0".freeze
10
+ VERSION = "0.3.0"
9
11
  end
10
12
  end