Load the error decoration gems by firing their autoloads

The lazy loading of error_highlight, did_you_mean, and syntax_suggest
claimed the load by an atomic exchange before the load started. So a
caller in another Ractor or thread during the load returned the
undecorated message without waiting for the load to complete.

    rs = 2.times.map do
      Ractor.new do
        nil.upcase
      rescue => e
        e.detailed_message(highlight: false)
      end
    end
    rs.each { puts it.value, "" }
    # One of them lacks the snippet by error_highlight

Load the gems with rb_autoload_load instead of rb_require_string, as a
constant reference would. The autoload's own synchronization makes
concurrent callers wait until the load is done. The flag to load the
gems only once is no longer needed: rb_autoload_load does nothing once
the gems are loaded, and whether to re-dispatch is now decided by whether
the load has just put a decorator above Exception#detailed_message.

This also makes the gems loaded through RubyGems' Kernel#require, so a
newer version of the gems installed by the user is activated, as it was
before they were lazily loaded.

[Bug #22442]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yusuke Endoh 2026-10-15 04:26:22 +09:00
parent ce3a306211
commit d193fd3217
Notes: git 2026-10-15 04:51:10 +00:00

35
error.c
View File

@ -1787,22 +1787,15 @@ exc_message(VALUE exc)
return rb_funcallv(exc, idTo_s, 0, 0);
}
// Whether error_highlight, did_you_mean, and syntax_suggest have already
// been loaded (lazily on the first error, or eagerly via Process.warmup).
// Ractors run in parallel, so the load is claimed by an atomic exchange.
static rb_atomic_t decoration_gems_loaded = 0;
static VALUE
load_decoration_gem(VALUE feature)
load_decoration_gem(VALUE name)
{
// The C-level require bypasses Kernel#require monkeypatches;
// displaying an error must not invoke or depend on them.
return rb_require_string(feature);
// Fire the autoload, which waits for a concurrent load to complete.
return rb_autoload_load(rb_cObject, (ID)name);
}
// Require error_highlight, did_you_mean, and syntax_suggest.
// rb_define_gem_modules registers an autoload entry for each enabled gem;
// disabled gems have no entry and are skipped.
// rb_define_gem_modules registers an autoload entry for each enabled gem.
static void
require_decoration_gems(void)
{
@ -1813,32 +1806,26 @@ require_decoration_gems(void)
VALUE saved_errinfo = rb_errinfo();
static const char *const gems[] = {"ErrorHighlight", "DidYouMean", "SyntaxSuggest"};
for (size_t i = 0; i < numberof(gems); i++) {
VALUE feature = rb_autoload_p(rb_cObject, rb_intern(gems[i]));
if (NIL_P(feature)) continue;
int state;
rb_protect(load_decoration_gem, feature, &state);
rb_protect(load_decoration_gem, (VALUE)rb_intern(gems[i]), &state);
(void)state;
}
rb_set_errinfo(saved_errinfo);
}
// Load the decoration gems on the first error display instead of at boot.
// In non-main Ractors rb_require_string delegates the require to the main
// In non-main Ractors rb_autoload_load delegates the load to the main
// Ractor, so this works from any Ractor.
// Returns whether the caller should re-dispatch to pick up the
// detailed_message decorators the gems prepend.
static bool
lazy_load_decoration_gems(VALUE exc)
{
if (ATOMIC_EXCHANGE(decoration_gems_loaded, 1)) return false;
// When entered through super from a decorator already sitting above
// this method, the caller decorates the result; re-dispatching would
// decorate it twice.
bool redispatch = rb_method_basic_definition_p(CLASS_OF(exc), id_detailed_message);
// Re-dispatch only if the load has just prepended a decorator.
bool before = rb_method_basic_definition_p(CLASS_OF(exc), id_detailed_message);
require_decoration_gems();
return redispatch;
bool after = rb_method_basic_definition_p(CLASS_OF(exc), id_detailed_message);
return before && !after;
}
// Load the decoration gems eagerly, e.g. from Process.warmup before a
@ -1847,8 +1834,6 @@ lazy_load_decoration_gems(VALUE exc)
void
rb_eager_load_detailed_message_extension(void)
{
if (ATOMIC_EXCHANGE(decoration_gems_loaded, 1)) return;
require_decoration_gems();
}