Project

General

Profile

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. 
 

Back