Skip to content

[GR-18163] Implement thread-safe autoloading #3078

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ Compatibility:
* Add `String#bytesplice` (#3039, @itarato).
* Add `String#byteindex` and `String#byterindex` (#3039, @itarato).
* Add implementations of `rb_proc_call_with_block`, `rb_proc_call_kw`, `rb_proc_call_with_block_kw` and `rb_funcall_with_block_kw` (#3068, @andrykonchin).
* Make `autoload` thread-safe, that is only publish the autoloaded constant once the file is fully loaded (#2431, #3040, @eregon).

Performance:

7 changes: 5 additions & 2 deletions spec/mspec/lib/mspec/matchers/have_class_variable.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
require 'mspec/matchers/variable'

class HaveClassVariableMatcher < VariableMatcher
self.variables_method = :class_variables
self.description = 'class variable'
self.description = 'class variable'

private def check(object, variable)
object.class_variable_defined?(variable)
end
end

module MSpecMatchers
8 changes: 6 additions & 2 deletions spec/mspec/lib/mspec/matchers/have_constant.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
require 'mspec/matchers/variable'

class HaveConstantMatcher < VariableMatcher
self.variables_method = :constants
self.description = 'constant'
self.description = 'constant'

private def check(object, variable)
# Differs from object.const_defined?(variable, false) for undefined constants
object.constants(false).include?(variable)
end
end

module MSpecMatchers
7 changes: 5 additions & 2 deletions spec/mspec/lib/mspec/matchers/have_instance_variable.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
require 'mspec/matchers/variable'

class HaveInstanceVariableMatcher < VariableMatcher
self.variables_method = :instance_variables
self.description = 'instance variable'
self.description = 'instance variable'

private def check(object, variable)
object.instance_variable_defined?(variable)
end
end

module MSpecMatchers
4 changes: 2 additions & 2 deletions spec/mspec/lib/mspec/matchers/variable.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class VariableMatcher
class << self
attr_accessor :variables_method, :description
attr_accessor :description
end

def initialize(variable)
@@ -9,7 +9,7 @@ def initialize(variable)

def matches?(object)
@object = object
@object.send(self.class.variables_method).include? @variable
check(@object, @variable)
end

def failure_message
16 changes: 14 additions & 2 deletions spec/ruby/core/kernel/autoload_spec.rb
Original file line number Diff line number Diff line change
@@ -111,7 +111,7 @@ def go
end
end

Kernel.autoload :KSAutoloadBB, "no_autoload.rb"
Kernel.autoload :KSAutoloadBB, "autoload_never_used.rb"

describe "Kernel.autoload" do
before :all do
@@ -133,8 +133,17 @@ def go
Kernel.autoload?(:KSAutoloadAA).should == @non_existent
end

it "sets the autoload constant in Object's constant table" do
it "sets the autoload constant in the caller class's constant table" do
Object.should have_constant(:KSAutoloadBB)
Kernel.should_not have_constant(:KSAutoloadBB)

module KernelSpecs
class AutoloadInCallerClass
Kernel.autoload :KSAutoloadCC, "autoload_never_used.rb"
AutoloadInCallerClass.should have_constant(:KSAutoloadCC)
Kernel.should_not have_constant(:KSAutoloadBB)
end
end
end

it "calls #to_path on non-String filenames" do
@@ -167,6 +176,9 @@ def go
it "returns the name of the file that will be autoloaded" do
Kernel.autoload :KSAutoload, "autoload.rb"
Kernel.autoload?(:KSAutoload).should == "autoload.rb"
Kernel.should_not have_constant(:KSAutoload)
Object.should_not have_constant(:KSAutoload)
self.class.should have_constant(:KSAutoload)
end

it "returns nil if no file has been registered for a constant" do
64 changes: 43 additions & 21 deletions spec/ruby/core/module/autoload_spec.rb
Original file line number Diff line number Diff line change
@@ -299,25 +299,28 @@ module ModuleSpecs::Autoload
autoload :RequiredDirectlyNoConstant, fixture(__FILE__, "autoload_required_directly_no_constant.rb")
end
@path = fixture(__FILE__, "autoload_required_directly_no_constant.rb")
@remove << :RequiredDirectlyNoConstant
@check = -> {
[
defined?(ModuleSpecs::Autoload::RequiredDirectlyNoConstant),
ModuleSpecs::Autoload.constants(false).include?(:RequiredDirectlyNoConstant),
ModuleSpecs::Autoload.const_defined?(:RequiredDirectlyNoConstant),
ModuleSpecs::Autoload.autoload?(:RequiredDirectlyNoConstant)
]
}
ScratchPad.record @check
@check.call.should == ["constant", true, true, @path]
@check.call.should == ["constant", true, @path]
$:.push File.dirname(@path)
begin
require "autoload_required_directly_no_constant.rb"
ensure
$:.pop
end
ScratchPad.recorded.should == [nil, true, false, nil]
@check.call.should == [nil, true, false, nil]
ScratchPad.recorded.should == [nil, false, nil]
@check.call.should == [nil, false, nil]

# undefined constant, CRuby 3.1 & 3.2 still do it here, but it is inconsistent, the constant should be removed
if ModuleSpecs::Autoload.constants(false).include?(:RequiredDirectlyNoConstant)
@remove << :RequiredDirectlyNoConstant
end
end
end

@@ -568,10 +571,17 @@ class LexicalScope
LexicalScope::DeclaredInParentDefinedInCurrent.should == :declared_in_parent_defined_in_current
end

# Basically, the parent autoload constant remains in a "undefined" state
self.autoload?(:DeclaredInParentDefinedInCurrent).should == nil
const_defined?(:DeclaredInParentDefinedInCurrent).should == false
-> { DeclaredInParentDefinedInCurrent }.should raise_error(NameError)
ruby_version_is ""..."3.1" do
# Basically, the parent autoload constant remains in a "undefined" state
self.constants(false).should.include?(:DeclaredInParentDefinedInCurrent)
end
ruby_version_is "3.1" do
# The autoload constant has been removed
self.constants(false).should_not.include?(:DeclaredInParentDefinedInCurrent)
end

ModuleSpecs::Autoload::LexicalScope.send(:remove_const, :DeclaredInParentDefinedInCurrent)
end
@@ -592,13 +602,11 @@ module Autoload
-> {
DeclaredInCurrentDefinedInParent
}.should complain(
/Expected .*autoload_callback.rb to define ModuleSpecs::Autoload::DeclaredInCurrentDefinedInParent but it didn't/,
verbose: true,
)
/Expected .*autoload_callback.rb to define ModuleSpecs::Autoload::DeclaredInCurrentDefinedInParent but it didn't/, verbose: true)

-> {
DeclaredInCurrentDefinedInParent
}.should_not complain(/.*/, verbose: true)
}.should_not complain(verbose: true)
self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil
const_defined?(:DeclaredInCurrentDefinedInParent).should == false
ModuleSpecs.const_defined?(:DeclaredInCurrentDefinedInParent).should == true
@@ -607,8 +615,9 @@ module Autoload
end
end

# like net/https used to do `module Net; autoload :OpenSSL, 'openssl'; end` before https://github.com/ruby/net-http/commit/369c3fd708
ruby_version_is "3.1" do
it "looks up in parent scope after failed autoload" do
it "looks up in parent scope when declared in current and defined in parent" do
@remove << :DeclaredInCurrentDefinedInParent
module ModuleSpecs::Autoload
ScratchPad.record -> {
@@ -617,10 +626,11 @@ module ModuleSpecs::Autoload

class LexicalScope
autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb")
-> { DeclaredInCurrentDefinedInParent }.should_not raise_error(NameError)
# Basically, the autoload constant remains in a "undefined" state
DeclaredInCurrentDefinedInParent.should == :declared_in_current_defined_in_parent
# The autoload constant has been removed
self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil
const_defined?(:DeclaredInCurrentDefinedInParent).should == false
self.constants(false).should_not.include?(:DeclaredInCurrentDefinedInParent)
-> { const_get(:DeclaredInCurrentDefinedInParent) }.should raise_error(NameError)
end

@@ -630,7 +640,7 @@ class LexicalScope
end

ruby_version_is ""..."3.1" do
it "and fails when finding the undefined autoload constant in the current scope when declared in current and defined in parent" do
it "fails when finding the undefined autoload constant in the current scope when declared in current and defined in parent" do
@remove << :DeclaredInCurrentDefinedInParent
module ModuleSpecs::Autoload
ScratchPad.record -> {
@@ -643,7 +653,7 @@ class LexicalScope
# Basically, the autoload constant remains in a "undefined" state
self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil
const_defined?(:DeclaredInCurrentDefinedInParent).should == false
self.should have_constant(:DeclaredInCurrentDefinedInParent)
self.constants(false).should.include?(:DeclaredInCurrentDefinedInParent)
-> { const_get(:DeclaredInCurrentDefinedInParent) }.should raise_error(NameError)
end

@@ -792,14 +802,12 @@ module ModuleSpecs::Autoload
-> { ModuleSpecs.autoload "a name", @non_existent }.should raise_error(NameError)
end

it "shares the autoload request across dup'ed copies of modules" do
require fixture(__FILE__, "autoload_s.rb")
@remove << :S
it "does not share the autoload request internals across dup'ed copies of modules" do
filename = fixture(__FILE__, "autoload_t.rb")
mod1 = Module.new { autoload :T, filename }
-> {
ModuleSpecs::Autoload::S = mod1
}.should complain(/already initialized constant/)
ModuleSpecs::Autoload::S = mod1
@remove << :S

mod2 = mod1.dup

mod1.autoload?(:T).should == filename
@@ -905,6 +913,20 @@ class ModuleSpecs::Autoload::Z < ModuleSpecs::Autoload::ZZ
t2_exc.should be_nil
end

it "blocks other threads until the file is done loading so no partial modules are seen" do
ModuleSpecs::Autoload.autoload :ThreadSafe, fixture(__FILE__, "autoload_thread_safe.rb")
barrier = ModuleSpecs::CyclicBarrier.new 2
ScratchPad.record(barrier)

thread = Thread.new { # starts the autoload
ModuleSpecs::Autoload::ThreadSafe.foo.should == 42
}
barrier.await
ModuleSpecs::Autoload::ThreadSafe.foo.should == 42 # ThreadSafe should only be published once the whole file is loaded

thread.join
end

# https://bugs.ruby-lang.org/issues/10892
it "blocks others threads while doing an autoload" do
file_path = fixture(__FILE__, "repeated_concurrent_autoload.rb")
20 changes: 20 additions & 0 deletions spec/ruby/core/module/fixtures/autoload_thread_safe.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module ModuleSpecs::Autoload
class ThreadSafe
def self.bar # to illustrate partial loading
end

barrier = ScratchPad.recorded
barrier.await

# the main thread should be waiting for this file to be fully loaded
Thread.pass until Thread.main.stop?
10.times do
Thread.pass
Thread.main.should.stop?
end

def self.foo
42
end
end
end
2 changes: 1 addition & 1 deletion spec/ruby/core/module/fixtures/classes.rb
Original file line number Diff line number Diff line change
@@ -475,7 +475,7 @@ def extend_object(obj)
end

class CyclicBarrier
def initialize(count = 1)
def initialize(count)
@count = count
@state = 0
@mutex = Mutex.new
2 changes: 1 addition & 1 deletion spec/ruby/library/syslog/constants_spec.rb
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@

it "includes the Syslog constants" do
@constants.each do |c|
Syslog::Constants.should have_constant(c)
Syslog::Constants.should.const_defined?(c, true)
end
end
end
2 changes: 0 additions & 2 deletions spec/tags/core/kernel/require_tags.txt
Original file line number Diff line number Diff line change
@@ -2,8 +2,6 @@ slow:Kernel#require (concurrently) blocks based on the path
slow:Kernel.require (concurrently) blocks based on the path
slow:Kernel#require ($LOADED_FEATURES) unicode_normalize is part of core and not $LOADED_FEATURES
slow:Kernel.require ($LOADED_FEATURES) unicode_normalize is part of core and not $LOADED_FEATURES
fails:Kernel#require (non-extensioned path) loads a .rb extensioned file when a C-extension file exists on an earlier load path
fails:Kernel#require (non-extensioned path) does not load a feature twice when $LOAD_PATH has been modified
slow:Kernel#require ($LOADED_FEATURES) complex, enumerator, rational, thread, ruby2_keywords are already required
slow:Kernel.require ($LOADED_FEATURES) complex, enumerator, rational, thread, ruby2_keywords are already required
slow:Kernel#require complex, enumerator, rational, thread, ruby2_keywords, fiber are already required
2 changes: 1 addition & 1 deletion src/main/java/org/truffleruby/core/kernel/KernelNodes.java
Original file line number Diff line number Diff line change
@@ -572,7 +572,7 @@ protected static RubyDynamicObject copyable(Object object, Object freeze,
final RubyClass selfMetaClass = metaClassNode.execute(node, object);
if (isSingletonProfile.profile(node, selfMetaClass.isSingleton)) {
final RubyClass newObjectMetaClass = lazySingletonClassNode.get(node).execute(newObject);
newObjectMetaClass.fields.initCopy(selfMetaClass);
newObjectMetaClass.fields.initCopy(getContext(node), selfMetaClass, node);
}

final boolean copyFrozen = freeze instanceof Nil;
8 changes: 8 additions & 0 deletions src/main/java/org/truffleruby/core/klass/ClassNodes.java
Original file line number Diff line number Diff line change
@@ -335,6 +335,14 @@ protected Object getSuperClass(RubyClass rubyClass) {
}
}

@Primitive(name = "class_non_singleton_class")
public abstract static class NonSingletonClassNode extends PrimitiveArrayArgumentsNode {
@Specialization
protected RubyClass nonSingletonClass(RubyClass rubyClass) {
return rubyClass.nonSingletonClass;
}
}

@CoreMethod(names = { "__allocate__", "__layout_allocate__" }, constructor = true, visibility = Visibility.PRIVATE)
public abstract static class AllocateNode extends CoreMethodArrayArgumentsNode {
@Specialization
Original file line number Diff line number Diff line change
@@ -22,13 +22,14 @@ public class ConstantLookupResult {
@CompilationFinal(dimensions = 1) private final Assumption[] assumptions;

public ConstantLookupResult(RubyConstant constant, Assumption... assumptions) {
assert constant == null || !(constant.isAutoload() && constant.getAutoloadConstant().isAutoloadingThread());
assert constant == null ||
!(constant.isAutoload() && constant.getAutoloadConstant().isAutoloadingThreadAndUnset());
this.constant = constant;
this.assumptions = assumptions;
}

public boolean isFound() {
return constant != null && !constant.isUndefined();
return constant != null;
}

public boolean isDeprecated() {
Loading