Bug #22293
closed`String#unicode_normalize` does not work in non-main Ractor
Description
This fails consistently since Ruby 3.0 (except for using #take instead of #value).
$ ruby -v -W:no-ex -e 'p Ractor.new{"aaa".unicode_normalize}.value'
ruby 4.1.0dev (2026-09-04T13:17:34Z master 3643154e6d) +PRISM [arm64-darwin25]
#<Thread:0x0000000123bf0008 run> terminated with exception (report_on_exception is true):
/opt/local/lib/ruby/4.1.0+4/unicode_normalize/normalize.rb:138:in 'UnicodeNormalize.normalize': can not access non-shareable objects in constant UnicodeNormalize::NF_HASH_C by non-main ractor. (Ractor::IsolationError)
from -e:1:in 'String#unicode_normalize'
from -e:1:in 'block in <main>'
-e:1:in 'Ractor#value': thrown by remote Ractor. (Ractor::RemoteError)
from -e:1:in '<main>'
/opt/local/lib/ruby/4.1.0+4/unicode_normalize/normalize.rb:138:in 'UnicodeNormalize.normalize': can not access non-shareable objects in constant UnicodeNormalize::NF_HASH_C by non-main ractor. (Ractor::IsolationError)
from -e:1:in 'String#unicode_normalize'
from -e:1:in 'block in <main>'
Updated by duerst (Martin Dürst) 2 months ago
Just some comments to hopefully help this issue move forward.
unicode_normalize uses two hashes, UnicodeNormalize::NF_HASH_C and UnicodeNormalize::NF_HASH_D. These hashes are used to cache results of composition (NF_HASH_C) or decomposition (NF_HASH_D). They are updated with default procs when a key is not found (i.e. when the result of a normalization operation isn't already in the hash, and therefore has to be calculated.)
As long as the default proc isn't triggered, using unicode_normalize in parallel in two different ractors (or threads, or fibers, or anything else of that sort) is fine. But when the default proc is executed, the hash can change.
If the two key for which the default proc is executed in parallel are the same, then resulting value will also be the same (because Unicode normalization is deterministic).
I do not know the implementation of Hash in detail. If it can be used in parallel in threads and the like, it should be possible to get unicode_normalize to work in all Ractors in parallel. Potentially, just declaring the involved hashes as sharable could be enough.
Updated by ko1 (Koichi Sasada) 25 days ago
- Status changed from Open to Closed
Applied in changeset git|6ca4a120328a84a23c57af268bd7a6af5520e151.
[Bug #22293] Make String#unicode_normalize work in non-main Ractors
UnicodeNormalize.normalize and .normalized? read three constants that are
not shareable, so any call from a non-main Ractor raised
Ractor::IsolationError:
-
NF_HASH_C / NF_HASH_D, the memoization caches. They cannot be made
shareable because a shareable Hash cannot be written to. Keep them per
Ractor instead: normalization is deterministic, so recomputing an entry
in another Ractor always gives the same string, and the cache is only a
cache. The DoS bound of MAX_HASH_LENGTH now applies per Ractor. -
UNICODE_ENCODINGS, which is splatted in a
whenclause on the path for
UTF-16, UTF-32, GB18030 and UCS-2/4BE. Freeze it.
The other constants are already shareable on master: tables.rb is
frozen_string_literal with frozen tables, and Regexp instances are frozen
since cb03bf43eb.
Keeping the caches keeps main Ractor performance: dropping them instead
costs 2.7x to 11x on real text, and asking the main Ractor for an entry
costs 19.4us per round trip against 3.8us to just compute it. The per
Ractor lookup adds about 60ns per call; normalized? fetches its cache
lazily so a string without a match does not pay it.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com