Merge pull request #43071 from rails/descendants-tracker

Remove autoloading logic from AS::DescendantsTracker
This commit is contained in:
Xavier Noria 2021-08-23 00:47:57 +02:00 committed by GitHub
commit a10611c5a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 109 additions and 108 deletions

@ -57,6 +57,11 @@ def eager_load?(path)
# accessors of each engine. # accessors of each engine.
mattr_accessor :_eager_load_paths, default: Set.new mattr_accessor :_eager_load_paths, default: Set.new
# If reloading is enabled, this private set holds autoloaded classes tracked
# by the descendants tracker. It is populated by an on_load callback in the
# main autoloader. Used to clear state.
mattr_accessor :_autoloaded_tracked_classes, default: Set.new
# An array of qualified constant names that have been loaded. Adding a name # An array of qualified constant names that have been loaded. Adding a name
# to this array will cause it to be unloaded the next time Dependencies are # to this array will cause it to be unloaded the next time Dependencies are
# cleared. # cleared.

@ -9,6 +9,7 @@ module ZeitwerkIntegration # :nodoc: all
module Decorations module Decorations
def clear def clear
Dependencies.unload_interlock do Dependencies.unload_interlock do
_autoloaded_tracked_classes.clear
Rails.autoloaders.main.reload Rails.autoloaders.main.reload
rescue Zeitwerk::ReloadingDisabledError rescue Zeitwerk::ReloadingDisabledError
raise "reloading is disabled because config.cache_classes is true" raise "reloading is disabled because config.cache_classes is true"

@ -21,17 +21,20 @@ def descendants(klass)
arr arr
end end
def clear def clear(only: nil)
if defined? ActiveSupport::Dependencies if only.nil?
@@direct_descendants.each do |klass, descendants| @@direct_descendants.clear
if Dependencies.autoloaded?(klass) return
@@direct_descendants.delete(klass) end
else
descendants.reject! { |v| Dependencies.autoloaded?(v) } @@direct_descendants.each do |klass, direct_descendants_of_klass|
if only.member?(klass)
@@direct_descendants.delete(klass)
else
direct_descendants_of_klass.reject! do |direct_descendant_of_class|
only.member?(direct_descendant_of_class)
end end
end end
else
@@direct_descendants.clear
end end
end end

@ -1,33 +1,49 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative "abstract_unit"
require "set" require "set"
require "active_support/descendants_tracker"
module DescendantsTrackerTestCases class DescendantsTrackerTest < ActiveSupport::TestCase
class Parent setup do
extend ActiveSupport::DescendantsTracker @original_state = ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").dup
@original_state.each { |k, v| @original_state[k] = v.dup }
ActiveSupport::DescendantsTracker.clear
eval <<~RUBY
class Parent
extend ActiveSupport::DescendantsTracker
end
class Child1 < Parent
end
class Child2 < Parent
end
class Grandchild1 < Child1
end
class Grandchild2 < Child1
end
RUBY
end end
class Child1 < Parent teardown do
ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").replace(@original_state)
%i(Parent Child1 Child2 Grandchild1 Grandchild2).each do |name|
DescendantsTrackerTest.send(:remove_const, name)
end
end end
class Child2 < Parent test ".descendants" do
end
class Grandchild1 < Child1
end
class Grandchild2 < Child1
end
ALL = [Parent, Child1, Child2, Grandchild1, Grandchild2]
def test_descendants
assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants
assert_equal_sets [Grandchild1, Grandchild2], Child1.descendants assert_equal_sets [Grandchild1, Grandchild2], Child1.descendants
assert_equal_sets [], Child2.descendants assert_equal_sets [], Child2.descendants
end end
def test_descendants_with_garbage_collected_classes test ".descendants with garbage collected classes" do
# The Ruby GC (and most other GCs for that matter) are not fully precise. # The Ruby GC (and most other GCs for that matter) are not fully precise.
# When GC is run, the whole stack is scanned to mark any object reference # When GC is run, the whole stack is scanned to mark any object reference
# in registers. But some of these references might simply be leftovers from # in registers. But some of these references might simply be leftovers from
@ -48,46 +64,35 @@ def test_descendants_with_garbage_collected_classes
assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants
end end
def test_direct_descendants test ".direct_descendants" do
assert_equal_sets [Child1, Child2], Parent.direct_descendants assert_equal_sets [Child1, Child2], Parent.direct_descendants
assert_equal_sets [Grandchild1, Grandchild2], Child1.direct_descendants assert_equal_sets [Grandchild1, Grandchild2], Child1.direct_descendants
assert_equal_sets [], Child2.direct_descendants assert_equal_sets [], Child2.direct_descendants
end end
def test_subclasses test ".subclasses" do
[Parent, Child1, Child2].each do |klass| [Parent, Child1, Child2].each do |klass|
assert_equal klass.direct_descendants, klass.subclasses assert_equal klass.direct_descendants, klass.subclasses
end end
end end
def test_clear test ".clear deletes all state" do
mark_as_autoloaded(*ALL) do ActiveSupport::DescendantsTracker.clear
ActiveSupport::DescendantsTracker.clear assert_empty ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants")
ALL.each do |k| end
assert_empty ActiveSupport::DescendantsTracker.descendants(k)
end test ".clear(only) deletes the given classes only" do
end ActiveSupport::DescendantsTracker.clear(only: Set[Child2, Grandchild1])
assert_equal_sets [Child1, Grandchild2], Parent.descendants
assert_equal_sets [Grandchild2], Child1.descendants
assert_equal_sets [Child1], Parent.direct_descendants
assert_equal_sets [Grandchild2], Child1.direct_descendants
end end
private private
def assert_equal_sets(expected, actual) def assert_equal_sets(expected, actual)
assert_equal Set.new(expected), Set.new(actual) assert_equal Set.new(expected), Set.new(actual)
end end
def mark_as_autoloaded(*klasses)
# If ActiveSupport::Dependencies is not loaded, forget about autoloading.
# This allows using AS::DescendantsTracker without AS::Dependencies.
if defined? ActiveSupport::Dependencies
old_autoloaded = ActiveSupport::Dependencies.autoloaded_constants.dup
ActiveSupport::Dependencies.autoloaded_constants = klasses.map(&:name)
end
old_descendants = ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").dup
old_descendants.each { |k, v| old_descendants[k] = v.dup }
yield
ensure
ActiveSupport::Dependencies.autoloaded_constants = old_autoloaded if defined? ActiveSupport::Dependencies
ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").replace(old_descendants)
end
end end

@ -1,36 +0,0 @@
# frozen_string_literal: true
require_relative "abstract_unit"
require "active_support/descendants_tracker"
require "active_support/dependencies"
require_relative "descendants_tracker_test_cases"
class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase
include DescendantsTrackerTestCases
def test_clear_with_autoloaded_parent_children_and_grandchildren
mark_as_autoloaded(*ALL) do
ActiveSupport::DescendantsTracker.clear
ALL.each do |k|
assert_empty ActiveSupport::DescendantsTracker.descendants(k)
end
end
end
def test_clear_with_autoloaded_children_and_grandchildren
mark_as_autoloaded Child1, Grandchild1, Grandchild2 do
ActiveSupport::DescendantsTracker.clear
assert_equal_sets [Child2], Parent.descendants
assert_equal_sets [], Child2.descendants
end
end
def test_clear_with_autoloaded_grandchildren
mark_as_autoloaded Grandchild1, Grandchild2 do
ActiveSupport::DescendantsTracker.clear
assert_equal_sets [Child1, Child2], Parent.descendants
assert_equal_sets [], Child1.descendants
assert_equal_sets [], Child2.descendants
end
end
end

@ -1,19 +0,0 @@
# frozen_string_literal: true
require_relative "abstract_unit"
require "active_support/descendants_tracker"
require_relative "descendants_tracker_test_cases"
class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase
include DescendantsTrackerTestCases
# Regression test for #8422. https://github.com/rails/rails/issues/8442
def test_clear_without_autoloaded_singleton_parent
mark_as_autoloaded do
parent_instance = Parent.new
parent_instance.singleton_class.descendants
ActiveSupport::DescendantsTracker.clear
assert_not ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class)
end
end
end

@ -2,6 +2,8 @@
require "active_support/core_ext/string/inflections" require "active_support/core_ext/string/inflections"
require "active_support/core_ext/array/conversions" require "active_support/core_ext/array/conversions"
require "active_support/descendants_tracker"
require "active_support/dependencies"
require "zeitwerk" require "zeitwerk"
module Rails module Rails
@ -27,6 +29,13 @@ module Finisher
unless config.cache_classes unless config.cache_classes
autoloader.enable_reloading autoloader.enable_reloading
autoloader.on_load do |_cpath, value, _abspath|
if value.is_a?(Class) && value.singleton_class < ActiveSupport::DescendantsTracker
ActiveSupport::Dependencies._autoloaded_tracked_classes << value
end
end
autoloader.on_unload do |_cpath, value, _abspath| autoloader.on_unload do |_cpath, value, _abspath|
value.before_remove_const if value.respond_to?(:before_remove_const) value.before_remove_const if value.respond_to?(:before_remove_const)
end end
@ -170,7 +179,9 @@ def self.complete(_state)
# added in the hook are taken into account. # added in the hook are taken into account.
initializer :set_clear_dependencies_hook, group: :all do |app| initializer :set_clear_dependencies_hook, group: :all do |app|
callback = lambda do callback = lambda do
ActiveSupport::DescendantsTracker.clear ActiveSupport::DescendantsTracker.clear(
only: ActiveSupport::Dependencies._autoloaded_tracked_classes
)
ActiveSupport::Dependencies.clear ActiveSupport::Dependencies.clear
end end

@ -1,5 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require "set"
require "isolation/abstract_unit" require "isolation/abstract_unit"
require "active_support/dependencies/zeitwerk_integration" require "active_support/dependencies/zeitwerk_integration"
@ -355,6 +356,36 @@ def Foo.before_remove_const
assert $before_remove_const_invoked assert $before_remove_const_invoked
end end
test "reloading clears autoloaded tracked classes" do
eval <<~RUBY
class Parent
extend ActiveSupport::DescendantsTracker
end
RUBY
app_file "app/models/child.rb", <<~RUBY
class Child < #{self.class.name}::Parent
end
RUBY
app_file "app/models/grandchild.rb", <<~RUBY
class Grandchild < Child
end
RUBY
boot
assert Grandchild
# Preconditions, we add some redundancy about descendants tracking.
assert_equal Set[Child, Grandchild], ActiveSupport::Dependencies._autoloaded_tracked_classes
assert_equal [Child, Grandchild], Parent.descendants
Rails.application.reloader.reload!
assert_empty ActiveSupport::Dependencies._autoloaded_tracked_classes
assert_equal [], Parent.descendants
end
test "autoloaders.logger=" do test "autoloaders.logger=" do
boot boot