Feature #22226
Updated by ko1 (Koichi Sasada) about 2 months ago
## Abstract
Every class/module records the Ractor that created it as its *owner*. Only the owner Ractor
can modify it. Reading is unchanged and allowed from any Ractor.
This replaces several ad-hoc "main Ractor only" rules with a single rule keyed on the creator.
It **relaxes** the rules for classes a Ractor creates itself, and **tightens** them for classes
created by somebody else.
No Ruby-level API is added: ownership is recorded internally and is not exposed.
---
## 1. Motivation
### 1.1 The current rules ask the wrong question
Today's restrictions ask *"are you the main Ractor?"*. They never ask *"did you create this class?"*.
The result is inconsistent in both directions.
A non-main Ractor may freely redefine **any** class in the process:
```ruby
class C; end
Ractor.new { C.define_method(:m) { 1 }; :done }.value #=> :done (works today)
Ractor.new { class String; def foo = 1; end; :done }.value #=> :done (works today)
Ractor.new { def f = 42; f() }.value #=> 42 (works today; defines Object#f)
Ractor.new { Object.send(:remove_const, :FOO); :done }.value #=> :done (works today)
```
...but it may **not** set an instance variable on a class it created itself:
```ruby
Ractor.new {
k = Class.new # nobody else can even name this class yet
k.instance_variable_set(:@iv, 1)
}.value
#=> Ractor::IsolationError:
# can not set instance variables of classes/modules by non-main Ractors
```
...nor give it an unshareable constant:
```ruby
Ractor.new {
k = Class.new
k.const_set(:X, "str".dup)
}.value
#=> Ractor::IsolationError:
# can not set constants with non-shareable objects by non-main Ractors
```
The first group is unrestricted although it affects the whole process.
The second group is forbidden although it affects nothing but the Ractor doing it.
The rules are exactly inverted with respect to who is actually affected.
### 1.2 A Ractor's classes can be redefined under it by another Ractor
Because any Ractor may modify any class, the code a Ractor runs can be changed by somebody else
at any moment:
```ruby
Ractor.new { class String; def upcase = :patched; end; :done }.value
"abc".upcase #=> :patched # evaluated in the *main* Ractor
```
```ruby
FOO = 1
Ractor.new { Object.send(:remove_const, :FOO); :removed }.value
FOO #=> NameError: uninitialized constant FOO
```
Both of these run fine today. There is no way for a Ractor to rely on the classes it uses.
### 1.3 Class internals have no single writer
`m_tbl`, `const_tbl` and the class fields can be written by any Ractor today. This means the
implementation cannot assume a single writer for any class, which stands in the way of
synchronization and caching work (and it is also how the `cvar_overtaken()` bug below arose:
a *read* path physically deletes a table entry, potentially of another Ractor's class).
---
## 2. Proposal
A class/module records its creator Ractor as its **owner**. Only the owner may:
* define/remove/undef methods, `alias`, change method visibility
* `include`/`prepend` into it, and `Module#refine` targeting it
* define/remove constants, register `autoload`
* set instance variables on the class/module object
Non-owner Ractors get `Ractor::IsolationError`.
**Reading is completely unchanged**: method calls, instantiation, subclassing, constant reads
and instance-variable reads all work from any Ractor as before.
In exchange, the previous "main Ractor only" rules for class instance variables and unshareable
constant values become "**owner Ractor only**". A Ractor gets full use of the classes it creates.
Since all classes/modules created at boot or by main-Ractor code (including everything loaded by
`require`) are owned by the main Ractor, this is equivalent to today's rules for programs that do
not create classes inside non-main Ractors.
---
## 3. Examples
### 3.1 Newly allowed: a Ractor can fully use the classes it creates
Instance variables on a class the Ractor created:
```ruby
Ractor.new {
k = Class.new
k.instance_variable_set(:@iv, 1) # was: IsolationError, even for a shareable value!
k.instance_variable_set(:@iv, "str".dup) # was: IsolationError
k.instance_variable_get(:@iv) #=> "str"
}.value
```
**Unshareable values in constants** of a class the Ractor created. This was the main Ractor's
privilege before; now every Ractor has it for its own classes:
```ruby
Ractor.new {
k = Class.new
k::BUF = "buffer".dup # unshareable value; was: IsolationError
k::BUF << "!" # ...and the owner can freely use it
k::BUF #=> "buffer!"
}.value
Ractor.new {
k = Class.new
k.const_set(:LIST, [1, 2, 3]) # an unfrozen Array is unshareable too
k::LIST << 4
k::LIST #=> [1, 2, 3, 4]
}.value
```
The value stays private to the owner -- reading it from another Ractor raises, which is exactly
the rule that applied to the main Ractor before:
```ruby
port = Ractor::Port.new
r = Ractor.new(port) { |pt| k = Class.new; k::BUF = "buffer".dup; pt << k; Ractor.receive }
k = port.receive
k::BUF
#=> Ractor::IsolationError: can not access non-shareable objects in constant
# #<Class:0x...>::BUF of a class/module created by another Ractor.
```
A *shareable* value in the same position is readable from anywhere, as before:
```ruby
r = Ractor.new(port) { |pt| k = Class.new; k::N = 42; pt << k; Ractor.receive }
k = port.receive
k::N #=> 42
```
Classes defined under a module the Ractor created itself work fully:
```ruby
Ractor.new {
mod = Module.new
mod.const_set(:Foo, Class.new { def m = :ok })
mod::Foo.new.m #=> :ok
}.value
```
### 3.2 Newly prohibited: modifying a class created by another Ractor
```ruby
class C; end
module M; end
Ractor.new { def f = 42 }.value
#=> can not modify Object because it is created by another Ractor
# (top-level `def` defines a private method on Object)
Ractor.new { class String; def foo = 1; end }.value
#=> can not modify String because it is created by another Ractor
Ractor.new { C.define_method(:m) { 1 } }.value
Ractor.new { C.send(:alias_method, :a, :inspect) }.value
Ractor.new { C.send(:private, :inspect) }.value
Ractor.new { C.send(:undef_method, :inspect) }.value
Ractor.new { C.include(M) }.value
Ractor.new { C.prepend(M) }.value
Ractor.new { Module.new { refine(C) { def z = 1 } } }.value
Ractor.new { C.autoload(:Zz, "zz") }.value
#=> can not modify C because it is created by another Ractor
Ractor.new { C.const_set(:X, 1) }.value # note: even a *shareable* value now raises
#=> can not set constants of classes/modules created by another Ractor
Ractor.new { class TopCls; end }.value # a top-level class name...
Ractor.new { module TopMod; end }.value # ...and a top-level module name
#=> can not set constants of classes/modules created by another Ractor
# (both write a constant into Object)
Ractor.new { Object.send(:remove_const, :FOO) }.value
#=> can not modify Object because it is created by another Ractor
Ractor.new { C.instance_variable_set(:@iv, 1) }.value
#=> can not set instance variables of classes/modules created by another Ractor
```
Patterns that stop working:
```ruby
# a registry in Class#inherited that writes into the superclass
Base = Class.new { def self.inherited(sub) = const_set(:"Sub#{sub.object_id}", sub) }
Ractor.new { Class.new(Base) }.value
#=> can not set constants of classes/modules created by another Ractor
# lazy definition on first touch
M2 = Module.new { def self.const_missing(n) = const_set(n, Class.new) }
Ractor.new { M2::Foo }.value
#=> can not set constants of classes/modules created by another Ractor
```
Both work if the definition happens before the Ractor is spawned (eager loading).
### 3.3 Unchanged
```ruby
Ractor.new { "abc".upcase }.value #=> "ABC"
Ractor.new { C.new }.value # instantiation
Ractor.new { Class.new(String) { def m = :ok }.new.m }.value #=> :ok (subclassing is creation)
Ractor.new { require "time"; Time.now.respond_to?(:xmlschema) }.value #=> true
```
`require` from a non-main Ractor keeps working because `Ractor#require` performs the load on the
main Ractor, so the library's classes are defined by (and owned by) the main Ractor.
`def` on an object a Ractor created is also unaffected -- an ordinary object's singleton class is
created by, and owned by, the Ractor that triggers it:
```ruby
Ractor.new { o = Object.new; def o.f = :ok; o.f }.value #=> :ok
```
### 3.4 Summary table
| operation from a non-main Ractor | today | proposal |
|---|---|---|
| define a method on a foreign class | OK | **IsolationError** |
| top-level `def` | OK | **IsolationError** |
| `include`/`prepend`/`refine` a foreign class | OK | **IsolationError** |
| set a **shareable** constant on a foreign class | OK | **IsolationError** |
| set an **unshareable** constant on a foreign class | IsolationError | IsolationError |
| `remove_const` / `autoload` on a foreign class | OK | **IsolationError** |
| set an ivar on a foreign class | IsolationError | IsolationError |
| define a method on its **own** class | OK | OK |
| set a **shareable** constant on its **own** class | OK | OK |
| set an **unshareable** constant on its **own** class | IsolationError | **OK** |
| set an ivar on its **own** class | IsolationError | **OK** |
| read / call / instantiate / subclass any class | OK | OK |
---
## 4. How the proposal answers the motivation
* **1.1 (rules ask the wrong question)** -- the rules now ask "did you create this?" instead of
"are you main?". Every restriction is on the class of somebody else, and every relaxation is on
your own class. The two inverted cases in 1.1 both flip to the right side.
The old "main Ractor only" rules become the special case *"boot-time classes are owned by the
main Ractor"*, so they are no longer separate rules.
* **1.2 (classes changed under you)** -- no Ractor can modify a class it did not create, so the
classes a Ractor uses cannot be redefined by another Ractor.
* **1.3 (no single writer)** -- every class/module now has exactly one Ractor which can write its
method table, constant table and fields.
---
## 5. What this does **not** give
Ownership bounds *who may write*, not *what readers may see*. Reads stay unsynchronized and a
class modification is not atomic, so a non-owner Ractor can still observe a class in the middle
of being modified by its owner: after the first `def` of a `class ... end` body but before the
second, or while `include`/`prepend` is rewiring the ancestor chain.
The guarantee is **a single writer per class/module**, not a consistent view for readers.
This proposal is not, and does not claim to be, full isolation of class state.
---
## 6. Details
* **Singleton classes / metaclasses** are owned by the owner of the object they are attached to,
not by the Ractor that happened to trigger their lazy creation. `def C.foo` is allowed exactly
for the owner of `C`:
```ruby
class C; end
Ractor.new { C.singleton_class; :touched }.value # another Ractor touches it first
class << C; def foo = :ok; end # still fine in main
C.foo #=> :ok
Ractor.new { class << C; def bar = 1; end }.value #=> IsolationError
```
* **Terminated owner**: a class whose owner Ractor has finished becomes permanently read-only for
everybody, including the main Ractor.
```ruby
K = Ractor.new { Class.new { def x = :made_in_ractor } }.value
K.new.x #=> :made_in_ractor (reading is fine)
K.define_method(:y) { 1 } #=> IsolationError (nobody owns it any more)
```
There is intentionally no API to look up or transfer ownership.
* **Class variables** are *not* covered by the relaxation. They are shared across the whole
inheritance chain and the class they are physically stored in can migrate over time
(`cvar_overtaken`), so no single owner can be defined for them. Writes still require the main
Ractor, and additionally must not cross the ownership boundary (the class actually written into
must be owned), so class fields keep a single writer.
* **Copying a foreign class**: `Class#dup`/`clone` of a class created by another Ractor produces a
copy owned by the copying Ractor. It raises if the source's constants or instance variables
refer to unshareable objects (they would leak across Ractors); copying classes holding only
shareable values works. This gives a mutation-free alternative to monkey-patching:
```ruby
S = Class.new; S.const_set(:OK, 1)
Ractor.new { k = S.dup; k.define_method(:m) { 1 }; k.new.m }.value #=> 1
```
* **Top-level class and module names**: `class Foo; end` / `module Foo; end` at the top level of a
non-main Ractor writes a constant into `Object`, so both are prohibited. Define them under a
module the Ractor creates itself instead
(`mod = Module.new; mod.const_set(:Foo, Class.new)`). Integration with `Ruby::Box` is future work.
* **Bug fix included**: `cvar_overtaken()` physically deleted a duplicated cvar entry from the
`front` class, and that path is reachable from *read* operations (`rb_cvar_find`) -- i.e. a
cross-Ractor write triggered by a read. The clean-up is now skipped unless the current Ractor
owns `front`.
---
## 7. Incompatibility
Only code that modifies classes from a non-main Ractor is affected. Concretely, the following
raise `Ractor::IsolationError` where they used to work:
* method definition / `alias` / visibility change on a foreign class, including top-level `def`
* `include`/`prepend` into a foreign class, `refine` targeting one
* setting a **shareable** constant on a foreign class, `remove_const`, `autoload` registration
* `Class#inherited` and `const_missing`/`method_missing` hooks which mutate a foreign class
Migration is usually "define it before spawning the Ractor" (eager loading), or "create the class
inside the Ractor that uses it", or "`dup` the foreign class and modify the copy".
Programs which never define classes inside non-main Ractors are unaffected.
---
## 8. Open questions / future work
* `freeze` of a foreign class is still allowed. It is a behavior-changing write and should
probably be owner-only too.
* `Module#set_temporary_name` is not checked yet.
* Singleton class ownership is not transferred by `Ractor#send(move: true)`.
* Integration with `Ruby::Box`, so that non-main Ractors can define top-level class/module names.
* Should the error message for a class owned by the main Ractor say "owned by the main Ractor"
rather than "created by another Ractor"? The current wording is confusing for top-level `def`,
where the user never mentioned `Object`.
---
## 9. Implementation
https://github.com/ruby/ruby/pull/17913
The owner is stored in `rb_classext_t::owner_ractor` as the Ractor object; `0` means the main
Ractor, so single-Ractor programs get no additional GC edges. It is marked from the classext and
updated on compaction, so the comparison never sees a dangling or reused reference. Method-table
checks funnel through `rb_class_modify_check()`; constant/ivar/cvar checks replace the previous
`rb_ractor_main_p()` checks in `variable.c`. The class-ivar fast paths in `vm_insnhelper.c` switch
from "main Ractor" to "owner Ractor", which keeps them valid because the owner is the only writer
and its threads are serialized by the per-Ractor lock.
`make btest` (2053 tests), `make test-all` (35797 tests, 0 failures / 0 errors) and `make test-spec`
(32628 examples, 0 failures / 0 errors / 0 tagged) all pass. Three tests which relied on
cross-Ractor method definition were rewritten so that the owner Ractor performs the definition.
## Notes
* The Above description above was is written by Claude Code based on code from my explanation.
* The primary biggest motivation is to eliminate the special cases for the main Ractor. "main-Ractor" exceptions. The concept of an "owner Ractor" Ractor for classes and modules provides a general solution to such cases. classes/modules" solves this kind of exception.
* Another possible approach would be to idea for mutating the classes/modules is prohibit class and module mutations from them on non-main Ractors, since Ractors because most classes and modules of classes/modules are defined by on the loading time on the main Ractor during program loading. Ractor. However, this would it also prohibit prohibits `Class.new` in non-main Ractors, even though dynamically creating classes and it is a common programming pattern in on Ruby.