Skip to content

Ractor support - #741

Open
gmcgibbon wants to merge 6 commits into
ruby-i18n:masterfrom
Shopify:ractor_support
Open

Ractor support#741
gmcgibbon wants to merge 6 commits into
ruby-i18n:masterfrom
Shopify:ractor_support

Conversation

@gmcgibbon

@gmcgibbon gmcgibbon commented Aug 5, 2026

Copy link
Copy Markdown

Rails has recently started implementing experimental support for Ractors, and I noticed this library needs a few changes to properly be used on non-main Ractors. Here's the changes I made:

  • Introduce I18n.reserved_keys that uses an instance variable instead of RESERVED_KEYS and assigns and re-freezes keys.
  • Freeze I18n::INTERPOLATION_PATTERN and converts INTERPOLATION_PATTERNS_CACHE to a instance variable that assigns and re-freezes the cache.
  • Introduce I18n.normalized_key_cache that uses Ractor-local storage when not on the main Ractor.
  • Make I18n backends ractor sahreable.
  • Make I18n configs ractor sahreable via i18n/ractorize
  • Make I18n locale tags ractor sahreable.

We may want to look at refactoring other pieces of I18n to freeze + use class variables + eagerly memoize in order to make Ractor safe, but this looks sufficient for most of what Rails needs.

Comment thread lib/i18n/interpolate/ruby.rb Outdated
Replaces the mutable RESERVED_KEYS array with a frozen @reserved_keys
class ivar and deprecates the constant.
Freezes fallback instances so they are shareable across Ractors, and
avoids writing the class variable on read when no fallbacks are set.
INTERPOLATION_PATTERNS_CACHE) cannot be made shareable, so let's move it
to Ractor local storage. Falls back to the original ivar on Rubies
without Ractor support.
@gmcgibbon
gmcgibbon force-pushed the ractor_support branch 2 times, most recently from 520aadd to 231a3d4 Compare August 12, 2026 05:39
@gmcgibbon
gmcgibbon force-pushed the ractor_support branch 3 times, most recently from b99a69a to 672b299 Compare August 25, 2026 22:44
Remove the default block Proc from Concurrent::Hash.new so
Ractor.make_shareable can freeze the backend. Adds i18n/ractorize.rb
to add in hooks to eager load values and make them shareable so they may
be referenced on ractors.
Converts config values to class level variables, and loads them eagerly
and makes them shareable in i18n/ractorize.rb.
Uses class variables so locale tags may be referenced in ractors. Adds
constant to eager_load!
@gmcgibbon

Copy link
Copy Markdown
Author

@radar if you have time to provide some feedback on this, I would greatly appreciate it. Many essential libraries in Ruby are undergoing efforts to be Ractor safe, and it would be very helpful for I18n to do the same. Let me know what you think.

@yaroslav

yaroslav commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Hey guys!

Ran this branch through Audition and its skill in Claude, and verified everything below on Ruby 4.0.6. Two things look weird, according to them:

1. I18n.fallbacks raises from non-main Ractors

The comment above the reader says reads from non-main Ractors do not trigger isolation violations, but class-variable reads raise Ractor::IsolationError even when the value is shareable (unlike class-level ivars, which are readable whenever the value is shareable). Since Fiber[:i18n_fallbacks] is nil on a fresh Ractor, the reader always falls through to @@fallbacks:

require "i18n"
require "i18n/backend/fallbacks"
require "i18n/ractorize"

Ractor.new { I18n.fallbacks[:en] }.value
# the worker raises Ractor::IsolationError: can not access class
# variables from non-main Ractors (@@fallbacks from I18n)
# at fallbacks.rb:22 (Fiber[:i18n_fallbacks] || @@fallbacks)

Moving @@fallbacks to an ivar on the I18n singleton (like the Config state) fixes the read path, but the stored value then has to be deeply shareable, not just frozen: a Fallbacks with mappings frozen via the custom freeze still holds unfrozen arrays in @map, and reading an unshareable value out of a class-level ivar raises too. With Ractor.make_shareable in the writer instead of freeze, I verified the full chain works from a worker: a class-ivar Fallbacks.new(:"en-GB", :"de-AT" => :de) lookup of [:"de-AT"] returns [:"de-AT", :de, :"en-GB", :en] (the frozen instance computes per call without storing, as intended).

2. I18n::INTERPOLATION_PATTERN is no longer frozen

The PR description says it is frozen, and an earlier version of the interpolate changes had the .freeze (it is still visible in the first review thread's diff), but it went away when the patterns-cache approach was reworked:

INTERPOLATION_PATTERN = Regexp.union(DEFAULT_INTERPOLATION_PATTERNS)

It is deprecated, but any code still reading it from a non-main Ractor raises, since an unfrozen Regexp is not shareable (verified: the Regexp.union result is unshareable as is, shareable once frozen).

For the record, and probably intentional given the "most of what Rails needs" scope: I18n::Gettext's @@plural_keys and Backend::Simple::Implementation::MUTEX also remain main-Ractor-only.

@@fallbacks ||= I18n::Locale::Fallbacks.new
if Fiber.respond_to?(:[])
current = if Fiber.respond_to?(:[])
Fiber[:i18n_fallbacks] || @@fallbacks

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still uses the @@fallbacks class variable which can't be Ractor-safe in Ruby 4.0 (even if they will be in 4.1).

Action View reads it on every render because LookupContext's :locale detail calls I18n.fallbacks[I18n.locale] whenever I18n.respond_to?(:fallbacks), and it does once i18n/ractorize runs I18n.eager_load!, which loads this file. So rendering anything from a Ractor worker raises:

Ractor::IsolationError (can not access class variables from non-main Ractors (@@fallbacks from I18n)):
i18n/lib/i18n/backend/fallbacks.rb:22:in 'I18n.fallbacks'
rails/actionview/lib/action_view/lookup_context.rb:48:in 'block in <class:LookupContext>'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants