Project

General

Profile

Actions

Bug #22457

open

Implicit Ractor shareability for frozen objects is buggy

Bug #22457: Implicit Ractor shareability for frozen objects is buggy

Added by luke-gru (Luke Gruber) about 5 hours ago.

Status:
Open
Assignee:
Target version:
[ruby-core:127184]

Description

Background

To share objects between Ractors without passing them explicitly with Ractor#send, you can use shareable objects. Certain objects are always shareable (such as nil, true, false, Symbols, etc) but most aren't. To make an objects shareable, you can use Ractor.make_shareable. This freezes the object recursively and makes it possible to be shared across Ractors.

To make it easier for developers, we also made it possible to just #freeze your object, and if your object passes all shareability rules (all its references are also frozen), then it can also be shared. For example:

ALPHA = [:A, :B, :C].freeze
r = Ractor.new do
  puts ALPHA # this works, it can be accessed
end.join

On the other hand, this results in an error:

ALPHA = ('a'..'z').to_a.freeze
r = Ractor.new do
  puts ALPHA # The strings aren't frozen, so we get Ractor::IsolationError during access
end.join

The Issue

The way implicit shareability is implemented is that the Ractor that's doing the access first checks if the object has a shareable flag. If it does then it can access it, otherwise it traverses the object to check shareability for all references and sets the shareable flag once it determines the object is shareable (so it doesn't need to traverse again during the next lookup). There are a few issues with this approach:

  1. Setting the shareable flag sets a bit in a bitmap on the heap page of the other Ractor. This can race with other bits being set by this Ractor if it's running code or doing GC.

  2. Traversing the other Ractor's object graph can mutate the objects internally (such as calling str_make_independent). If the owning Ractor is using this object, then it can race (and crash).

Example

For an example of a crash showing issue 2:

# Repro: a non-owner Ractor reading a class ivar runs the full shareability traversal
# on the owner's object graph (rb_ivar_lookup -> rb_ractor_shareable_p ->
# rb_obj_traverse -> mark_shareable -> rb_str_make_independent).  Several readers do it
# at once on the same STR_SHARED strings, racing in str_make_independent_expand:
# one clears STR_SHARED and installs a new heap.ptr while another, holding the old
# pointer, re-tests the flags and frees it.
N_STR   = (ENV['N_STR']  || 20_000).to_i
N_RACT  = (ENV['N_RACT'] || 8).to_i
ROUNDS  = (ENV['ROUNDS'] || 50).to_i

BASE = ("abcdefghij" * 200).freeze   # long enough that substrings are STR_SHARED

ROUNDS.times do |round|
  k = Class.new do
    class << self; attr_accessor :v; end
  end
  Object.const_set(:"C#{round}", k)
  k.v = Array.new(N_STR) { |i| BASE[(i % 100)..].freeze }.freeze

  start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 0.05
  rs = N_RACT.times.map do
    Ractor.new(k, start) do |kls, t0|
      nil while Process.clock_gettime(Process::CLOCK_MONOTONIC) < t0
      a = kls.v              # <- traverses + promotes the owner's graph
      a.size + a[0].length
    end
  end
  rs.each(&:value)
  $stderr.print "." if round % 10 == 0
end
$stderr.puts "\nsurvived #{ROUNDS} rounds"

Possible Fixes

  1. We can remove implicit shareability for deeply frozen objects. If an object isn't shareable, access from another Ractor raises Ractor::IsolationError. Users must call Ractor.make_shareable explicitly. This is a backwards incompatible change.

  2. We can do the shareability traversal check and set in the owning Ractor's #freeze. This would only need to be done for objects that are directly pointed to by shareable objects like classes/modules. This allows the checking Ractor to simply check the shareable flag on the object's flags (FL_SHAREABLE). For example:

class C
  @iv = ["iv".freeze].freeze # During ivar set of a class, if the object is frozen, we do the shareability check graph walk
end

This means that each time a class/module sets an ivar, a cvar or a constant and the object is frozen, we need to walk all references (until we see a non-shareable object). This slows down these operations, but there may not be many of them and most of them are probably on boot. This also increases the number of shareable objects, because previously frozen objects are now set frozen + shareable. This makes no difference in single-ractor mode, but possibly causes more global GCs in multi-ractor mode.

  1. The checking Ractor blocks on read of a non-shareable, the owning Ractor performs the check then unblocks the checking Ractor with the result of the object's shareability. There are complications here because of global GC and the asynchronous nature of the message passing. It could also be slow if this continues to happen after boot. However, the "check shareability on read" semantics are preserved and it doesn't have the problem of making more objects shareable than we have to.

Code

I have a PR that implements solution 2 and @jhawthorn (John Hawthorn) is working on implementing solution 3.

Feedback

Let us know how you think this implicit shareability check should work or if we should even have it to begin with.


Related issues 1 (1 open — 0 closed)

Related to Ruby - Bug #22419: [BUG] "rb_objspace_reachable_objects_from_local: set is owned by another Ractor" when a non-main Ractor reads a class instance variable since f0439cdfa7OpenractorActions

Updated by luke-gru (Luke Gruber) about 4 hours ago Actions #1

  • Related to Bug #22419: [BUG] "rb_objspace_reachable_objects_from_local: set is owned by another Ractor" when a non-main Ractor reads a class instance variable since f0439cdfa7 added
Actions

Also available in: PDF Atom