skip to content

In Ruby, what makes an object Ractor-shareable, and how do Ractor.make_shareable and the shareable_constant_value comment fix a constant Ractors cannot read?

level: seniorimportance: should knowfreq 28%

answer

  1. frozen all the way down
  2. Ractor.shareable? answers true or false
  3. make_shareable freezes the graph in place
  4. copy: true leaves the original alone
  5. magic comment modes: none, literal

basics

~20 s

An object is shareable when it and everything it references is frozen, or it is inherently shareable (Integers, Symbols, classes). Ractor.make_shareable deep-freezes a graph, and # shareable_constant_value: literal does that for constants assigned from literals.

solid answer

~40 s

`Ractor.shareable?(obj)` is true for immutable values (Integers, Floats, Symbols, `nil`, `true`, `false`), for classes, modules and Ractors, and for objects that are frozen **and** reference only shareable objects. `["a", [1]].freeze` is not shareable because the inner array is mutable. A non-main Ractor reading a constant that points to an unshareable object gets `Ractor::IsolationError`. `Ractor.make_shareable(obj)` freezes `obj` and everything reachable from it, in place, and returns it; with `copy: true` it deep-copies first and leaves the original mutable. For constants, the `# shareable_constant_value: literal` magic comment deep-freezes literals assigned to constants below it and raises `Ractor::IsolationError` if a non-literal, unshareable value is assigned.

go deeper

for a junior

Remember the rule of thumb: numbers, symbols and deeply frozen objects can be shared between Ractors; ordinary mutable objects cannot.

for a middle

Explain why freeze is not enough for nested data, what Ractor.shareable? checks, and what make_shareable does to the original object with and without copy: true.

for a senior

Diagnose Ractor::IsolationError on constants, pick literal mode or make_shareable, and spot when freezing a shared cache would break the main Ractor's own code.

for a principal

Decide how a codebase prepares for Ractors: frozen-by-default constants, which files adopt the magic comment, and how to audit gems that keep mutable module-level state.

## The problem: a constant a worker cannot read Suppose the image-hash workers share a table of thresholds: ```ruby THRESHOLDS = { sharp: [0.8, 0.9], blurry: [0.1, 0.3] } r = Ractor.new { THRESHOLDS[:sharp].first } r.value # raises Ractor::RemoteError, cause: Ractor::IsolationError ``` The main Ractor can read `THRESHOLDS`, but a non-main Ractor may read a constant only if it refers to a **shareable** object, and this Hash is mutable. The error message says it can not access non-shareable objects in constant `Object::THRESHOLDS` by non-main Ractor. ## What shareable means `Ractor.shareable?(obj)` answers the question. An object is shareable when any number of Ractors can use it at once without risk: - **Always shareable:** Integers, Floats, Rationals, Complex numbers, Symbols, `nil`, `true`, `false`. - **Shareable by nature:** `Class` and `Module` objects and `Ractor` objects. - **Shareable when deeply frozen:** a String, Array, Hash, Struct or ordinary object is shareable only if it is frozen **and** every object it refers to (elements, keys, values, instance variables) is shareable too. ```ruby Ractor.shareable?("gray".freeze) # => true Ractor.shareable?(["gray".freeze, [1]].freeze) # => false, [1] is mutable Ractor.shareable?({ mode: :fast }.freeze) # => true ``` The trap is that `freeze` is shallow: freezing the outer Hash does not freeze the arrays inside it. ## Ractor.make_shareable `Ractor.make_shareable(obj, copy: false)` walks the object graph and makes it shareable: | Call | Original object | Returns | |---|---|---| | `Ractor.make_shareable(obj)` | frozen in place, together with everything it references | the same object | | `Ractor.make_shareable(obj, copy: true)` | left unchanged and still mutable | a deep-frozen copy | Parts that are already shareable are skipped. If something in the graph cannot be made shareable, for example an object whose class comes from a C extension that does not support it, the call raises `Ractor::Error`. Because the default mode freezes in place, calling it on an object that other code still mutates will break that code with `FrozenError`; `copy: true` avoids that at the cost of a copy. ```ruby THRESHOLDS = Ractor.make_shareable( { sharp: [0.8, 0.9], blurry: [0.1, 0.3] } ) Ractor.new { THRESHOLDS[:sharp].first }.value # => 0.8 ``` ## The shareable_constant_value magic comment Wrapping every constant is noisy, so Ruby provides a magic comment, `# shareable_constant_value:`, with four modes: 1. **`none`** (the default): no special treatment. 2. **`literal`**: a constant assigned a literal (and the literals nested in it) is deep-frozen; any other value must already be shareable, or Ruby raises `Ractor::IsolationError` ("cannot assign unshareable object to X"). 3. **`experimental_everything`**: every value assigned to a constant goes through `Ractor.make_shareable`, which can deep-freeze objects that belong to someone else, such as a gem's constant. 4. **`experimental_copy`**: every value is deep-copied and then made shareable, leaving the source alone. ```ruby # shareable_constant_value: literal THRESHOLDS = { sharp: [0.8, 0.9], blurry: [0.1, 0.3] } # deep-frozen CACHE = Object.new # raises Ractor::IsolationError at assignment ``` The directive applies to constants that follow it in the current scope and can appear more than once in a file. `Module#const_set` is not affected by it. ## Diagnosing it in practice When a Ractor fails, the caller sees `Ractor::RemoteError`; the useful information is in its `cause`: 1. Rescue `Ractor::RemoteError` around `value` or `join` and print `e.cause.class` and `e.cause.message`. 2. If the cause is `Ractor::IsolationError` and the message names a constant, check that constant with `Ractor.shareable?` in the main Ractor. 3. Walk its contents: the culprit is usually one nested mutable String, Array or Hash, or an object with a mutable instance variable. ## Choosing a fix - Prefer **`literal` mode** in files that define lookup tables for Ractor code: it freezes what should be frozen and fails loudly on anything else. - Use **`make_shareable`** for values built at runtime, and **`copy: true`** when the original must stay mutable. - If a constant holds a truly mutable cache, freezing it is the wrong fix: give each Ractor its own copy, or keep the cache in one Ractor and ask it over a port.

  • What is the difference between `Ractor.make_shareable(config)` and `Ractor.make_shareable(config, copy: true)` for the rest of the main Ractor's code?
    Without `copy:` the call freezes `config` and everything it references in place, so any later code in the main Ractor that mutates it raises `FrozenError`. With `copy: true` Ruby deep-copies first and freezes only the copy, so the original stays mutable and the returned object is the one to hand to other Ractors.
  • Under `# shareable_constant_value: literal`, why does `LIMITS = build_limits` raise even if `build_limits` returns a plain Hash?
    `literal` mode freezes only literals written directly in the assignment. A method call's result is not a literal, so Ruby instead checks that the value is already shareable and raises `Ractor::IsolationError` when it is not. Wrap it with `Ractor.make_shareable(build_limits)`, or return a deeply frozen Hash.

saying these in an interview costs you the question

  • Thinks a frozen Array holding mutable strings is shareable
  • Expects make_shareable without copy: to leave the original mutable
  • Believes the literal mode also deep-freezes values returned by methods
  • Says non-main Ractors can read any constant because constants are global
  • Treats experimental_everything as a safe default for every file