Issue #17592 has been updated by ko1 (Koichi Sasada).


This is a summary for tomorrow's dev-meeting by my understanding.

----

* ko1: This ticket proposal: To maintain global configuration (mutable information), class/module instance variables should be readable from other ractors.
 
```ruby=
module Foo
  singleton_class.attr_accessor :config
  Foo.config = {example: 42}.freeze
end

# Current
Ractor.new{ p Foo.config } # => IsolationError

# Proposal
Ractor.new do
  p Foo.config #=> {example: 42}
  p Foo.config = {example: 43}.freeze
    #=> IsolationError, beacuse it is read-only from non-main ractors
end
    
Foo.config = {example: 44}.freeze # allowed updating from the main ractor
```

* ko1: There are two concerns: (1) atomicity concern and (2) performance concern.

(1) Atomicity concern

If two or more ivars (named `@a` and `@b`) should be update atomic, but threre is no way to synchronize them.

```ruby=
class C
  @a = @b = 0
  def self.update
    # assertion: @a, @b should be equal
    @a += 1
    @b += 1
  end
  def self.vars
    [@a, @b]
  end
end
```

Main ractor can calls `C.update` and update ivars. A ractor can call `C.vars` and it can returns inconsist values (`@a != @b`).

The danger of this concern is relatively low because this example is very artificial. Maybe most of usecase is initialization at loading time and no other ractors read while mutating. Also there is no coupled variables (like `@a, @b`), there is no problem. For example, there is no problem with only one `@config` ivar which manages all configrations. In other words, two or more configurations `@configA`, `@configB`, ... can have an atomicity issue.

The following code is also artifitial example.

```ruby=
class Fib
  @a = @b = 1
  # @a and @b are successive parts of the Fibonacci sequence.
  def self.next
    @a, @b = @b, @a + @b
  end

  def values
    # it should return successive parts of the Fib seq.
    # == "Fib seq constraint"
    [@a, @b]
  end

  def self.eventloop
    loop{
      Ractor.receive; Fib.next
    }
  end
end

gen = Ractor.new(Ractor.current){|main| loop{ m << true } }
con = Ractor.new{ 
  p Fib.value #=> return values can violate "Fib seq constraint"
}
```

If a user misused as an above example (using class/module ivars for the mutable state repository and updating them with multiple ractors (via the main-ractor)), it is danger.

For this concern, we have several options.

* (a) there is no problem to introduce this feature because it is almost safe.
* (b) Ractor is designed to avoid such consistency issues even if it can be avoided by careful programming. So this feature should not be introduced.

(b) is my position, but I agree it is very conservative.

This proposal has advantage for compatibility because many existing code can use ivars for sharing the global configurations and there is no need to rewrite them (if they only refer to sharable objects).

For example, `pp` library has one global configuration: `sharing_detection` which is stored in a class instance variable.

https://github.com/ruby/ruby/blob/master/lib/pp.rb#L109

For Ractor, it was rewrote by using Ractor-local configuration, but it was not ideal modification but ad-hoc modification with existing tools.

If this proposal is introduced, we can revert the ad-hoc modification.

(2) Performance concern

To allow accessing ivars from other ractors, every ivar accesses to class/module should be synchronized. Current implementation doesn't need to synchronize this accesses.

For constants and method search, we implemented const cache and method cache mechanisms. Such cache mechanisms are reasonable because they are not frequently rewriting. On the other hands, ivars can be mutated more frequently and not sure the it is reasonable to have a cache mechanism for it.

----

ko1: Alternative proposal is using TVar.

Rewriting configuration example with TVar:

```ruby=
module Foo
  Config = Ractor::TVar.new{ {example: 42}.freeze }
end

Ractor.new do
  Ractor.atomically do
    p Foo::Config.value #=> {example: 42}
  end
  ...
  Ractor.atomically do
    Foo::Config.value = {example: 43}.freeze
    # modification is allowed within atomically block
  end
end

Ractor.atomically do
  Foo::Config.value = {example: 44}.freeze
end
```

The advantage of this example is it is clear that manipulating sharable state between Ractors because `Ractor.atomically` method is needed. Another way of saying this is that we can become more aware of creating a shared state. With instance variables, it is hard to figure out which instance variables are shared with multiple Ractors.

Disadvantages:

* Writing `Ractor.atomically` is long to type.
* Incompatible with older versions.

Configuration should not be changed frequently, so the performance should not be a problem.

```ruby=
module Foo
  Config = Ractor::TVar.new{ {example: 42}.freeze }
end

Ractor.new do
  p Foo::Config.value #=> {example: 42}
end
    
Ractor.atomically do
  Foo::Config.value = {example: 44}.freeze
end
```

Rewriting other examples with TVar.

```ruby=
class C
  A = Ractor::TVar.new 0
  B = Ractor::TVar.new 0

  def self.update
    # assertion: A.value and B.value should be equal
    Ractor.atomically do
      A.value += 1
      B.value += 1
    end
  end
  def self.vars
    Ractor.atomically do
      [A.value, B.value]
    end
  end
end
```

```ruby=
class Fib
  A = Ractor::TVar.new 1
  B = Ractor::TVar.new 1

  def self.next
    Ractor.atomically do
      A.value, B.value = B.value, A.value + B.value
    end
  end

  def values
    # it should return successive parts of the Fib seq.
    # == "Fib seq constraint"
    Ractor.atomically do
      [A.value, B.value]
    end
  end

  def self.eventloop
    loop{
      Ractor.receive; Fib.next
    }
  end
end

gen = Ractor.new(Ractor.current){|main| loop{ m << true } }
con = Ractor.new{ 
  p Fib.value #=> TVar allows us to keep constraint
}
Fib.eventloop

# NOTE: Using TVars, there is no reason to maintain states
# by a main-ractor, so gen ractor can access Fib.next directly.

gen = Ractor.new{loop{ Fib.next } }
con = Ractor.new{ 
  p Fib.value #=> TVar allows us to keep constraint
}
```


----------------------------------------
Feature #17592: Ractor should allowing reading shareable class instance variables
https://bugs.ruby-lang.org/issues/17592#change-90393

* Author: marcandre (Marc-Andre Lafortune)
* Status: Open
* Priority: Normal
* Assignee: ko1 (Koichi Sasada)
----------------------------------------
It would be very helpful if Ractor was allowing reading class instance variables from non-main Ractor.


Currently is raises an IsolationError:

```ruby
module Foo
  singleton_class.attr_accessor :config
  Foo.config = {example: 42}.freeze
end

Ractor.new { p Foo.config } # => IsolationError
```

This limitation makes it challenging to have an efficient way to store general configs, i.e. global data that mutated a few times when resources get loaded but it immutable afterwards, and needs to be read all the time.

Currently the only way to do this is to use a constant and use `remove_const` + `const_set` (which can not be made atomic easily).

I think that allowing reading only may be the best solution to avoid any race condition, e.g. two different Ractors that call `@counter += 1`.

The only 3 scenarios I see here are:
0) declare the constant hack the official way to store config-style data
1) allow reading of instance variables for shareable objects (as long as the data is shareable)
2) allow read-write

I prefer 1)



-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>