ractor-wrapper 0.2.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.
data/README.md CHANGED
@@ -1,123 +1,458 @@
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 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.)
6
9
 
7
10
  ## Quick start
8
11
 
9
12
  Install ractor-wrapper as a gem, or include it in your bundle.
10
13
 
11
- gem install ractor-wrapper
14
+ ```sh
15
+ gem install ractor-wrapper
16
+ ```
12
17
 
13
18
  Require it in your code:
14
19
 
15
- require "ractor/wrapper"
20
+ ```ruby
21
+ require "ractor/wrapper"
22
+ ```
16
23
 
17
24
  You can then create wrappers for objects. See the example below.
18
25
 
19
- `Ractor::Wrapper` requires Ruby 3.0.0 or later.
26
+ Ractor::Wrapper requires Ruby 4.0.0 or later.
27
+
28
+ ## What is Ractor::Wrapper?
29
+
30
+ For the most part, unless an object is _shareable_, which generally means
31
+ deeply immutable along with a few other restrictions, it cannot be accessed
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
+ +-------------------+ +----------------+
45
+
46
+ Ractor::Wrapper makes it possible for an ordinary non-shareable object to
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.
76
+
77
+ ### Net::HTTP example
78
+
79
+ The following example shows how to share a single Net::HTTP session object
80
+ among multiple Ractors.
20
81
 
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.)
24
-
25
- ## About Ractor::Wrapper
82
+ ```ruby
83
+ # Net::HTTP example
26
84
 
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.
85
+ require "ractor/wrapper"
86
+ require "net/http"
87
+
88
+ # Create a Net::HTTP session. Net::HTTP sessions are not shareable,
89
+ # so normally only one Ractor can access them at a time.
90
+ http = Net::HTTP.new("example.com")
91
+ http.start
92
+
93
+ # Create a wrapper around the session. This moves the session into an
94
+ # internal Ractor and listens for method call requests. By default, a
95
+ # wrapper serializes calls, handling one at a time, for compatibility
96
+ # with non-thread-safe objects.
97
+ wrapper = Ractor::Wrapper.new(http)
98
+
99
+ # At this point, the session object can no longer be accessed directly
100
+ # because it is now owned by the wrapper's internal Ractor.
101
+ # http.get("/whoops") # <= raises Ractor::MovedError
102
+
103
+ # However, you can access the session via the stub object provided by
104
+ # the wrapper. This stub proxies the call to the wrapper's internal
105
+ # Ractor. And it's shareable, so any number of Ractors can use it.
106
+ response = wrapper.stub.get("/")
107
+
108
+ # Here, we start two Ractors, and pass the stub to each one. Each
109
+ # Ractor can simply call methods on the stub as if it were the original
110
+ # connection object. Internally, of course, the calls are proxied to
111
+ # the original object via the wrapper, and execution is serialized.
112
+ r1 = Ractor.new(wrapper.stub) do |stub|
113
+ 5.times do
114
+ stub.get("/hello")
115
+ end
116
+ :ok
117
+ end
118
+ r2 = Ractor.new(wrapper.stub) do |stub|
119
+ 5.times do
120
+ stub.get("/ruby")
121
+ end
122
+ :ok
123
+ end
32
124
 
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.
125
+ # Wait for the two above Ractors to finish.
126
+ r1.join
127
+ r2.join
39
128
 
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.
129
+ # After you stop the wrapper, you can retrieve the underlying session
130
+ # object and access it directly again.
131
+ wrapper.async_stop
132
+ http = wrapper.recover_object
133
+ http.finish
134
+ ```
47
135
 
48
- ### Example usage
136
+ ### SQLite3 example
49
137
 
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.
138
+ The following example shows how to share a SQLite3 database among multiple
139
+ Ractors.
53
140
 
54
141
  ```ruby
55
- require "faraday"
56
- require "ractor/wrapper"
142
+ # SQLite3 example
57
143
 
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")
144
+ require "ractor/wrapper"
145
+ require "sqlite3"
146
+
147
+ # Create a SQLite3 database. These objects are not shareable, so
148
+ # normally only one Ractor can access them.
149
+ db = SQLite3::Database.new($my_database_path)
150
+
151
+ # Create a wrapper around the database. A SQLite3::Database object
152
+ # cannot be moved between Ractors, so we configure the wrapper to run
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.
156
+ wrapper = Ractor::Wrapper.new(db, use_current_ractor: true, threads: 2)
157
+
158
+ # At this point, the database object can still be accessed directly
159
+ # from the current Ractor because it hasn't been moved.
160
+ rows = db.execute("select * from numbers")
161
+
162
+ # You can also access the database via the stub object provided by the
163
+ # wrapper.
164
+ rows = wrapper.stub.execute("select * from numbers")
165
+
166
+ # Here, we start two Ractors, and pass the stub to each one. The
167
+ # wrapper's worker threads will handle the requests concurrently.
168
+ r1 = Ractor.new(wrapper.stub) do |stub|
169
+ 5.times do
170
+ stub.execute("select * from numbers")
73
171
  end
74
172
  :ok
75
173
  end
76
- r2 = Ractor.new(wrapper) do |w|
77
- 10.times do
78
- w.stub.get("/ruby")
174
+ r2 = Ractor.new(wrapper.stub) do |stub|
175
+ 5.times do
176
+ stub.execute("select * from numbers")
79
177
  end
80
178
  :ok
81
179
  end
82
180
 
83
181
  # Wait for the two above Ractors to finish.
84
- r1.take
85
- r2.take
182
+ r1.join
183
+ r2.join
86
184
 
87
- # After you stop the wrapper, you can retrieve the underlying
88
- # connection object and access it directly again.
185
+ # After stopping the wrapper, you can call the join method to wait for
186
+ # it to completely finish.
89
187
  wrapper.async_stop
90
- connection = wrapper.recover_object
91
- connection.get("/finally")
188
+ wrapper.join
189
+
190
+ # When running a wrapper with :use_current_ractor, you do not need to
191
+ # recover the object, because it was never moved. The recover_object
192
+ # method is not available.
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
92
245
  ```
93
246
 
94
- ### Features
95
-
96
- * Provides a method interface to an object running in a different Ractor.
97
- * 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.
103
- * Can gracefully shut down the wrapper and retrieve the original object.
104
-
105
- ### Caveats
106
-
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
- * 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)
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.
121
456
 
122
457
  ## Contributing
123
458
 
@@ -132,11 +467,11 @@ Development is done in GitHub at https://github.com/dazuma/ractor-wrapper.
132
467
 
133
468
  The library uses [toys](https://dazuma.github.io/toys) for testing and CI. To
134
469
  run the test suite, `gem install toys` and then run `toys ci`. You can also run
135
- unit tests, rubocop, and builds independently.
470
+ unit tests, rubocop, and build tests independently.
136
471
 
137
472
  ## License
138
473
 
139
- Copyright 2021 Daniel Azuma
474
+ Copyright 2021-2026 Daniel Azuma
140
475
 
141
476
  Permission is hereby granted, free of charge, to any person obtaining a copy
142
477
  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.4.0"
9
11
  end
10
12
  end