A strange behavior of Rails constant autoloading
A copy of UserCore has been removed from the module tree but is still active!
- Error was emitted from method
ActiveSupport::Dependencies.load_missing_constant- Method was called when a constant is missing inside some
Modulecontext. In this case:UserCore
- Method was called when a constant is missing inside some
Necessary code context
# app/models/user.rb
class User < ApplicationRecord
include UserCore
end
# app/models/concerns/user_core.rb
module UserCore
extend ActiveSupport::Concern
def admin?
role?(UserRole::ADMIN)
end
end
# app/enums/user_role.rb
module UserRole
include Enum
UserRole.define :NONMEMBER, 5
UserRole.define :USER, 10
...
UserRole.define :ADMIN, 50
end
Steps to reproduce
- In your rails console, enter:
u = User.first; u.admin?reload!; u.admin?
Why does this happen?
- When
User#admin?is called, lookup forUserRoleconstant happens. When it is found, it is cached in the module constantUserCore: i.e. return value ofUserCore.const_defined?('UserRole')istrue. - If
reload!is called,UserCoreis reloaded, butustill has the older version of the constant. Rails raises anArgumentErrorwith the messageA copy of xxx has been removed from the module tree but is still active!when you try to initiate a constant lookup from a stale version of the constant.reload!is calledUserCoreis replaced with a newer version andUserCore.const_defined?('UserRole')is no longertrue. Hence rails autoloading ofUserRoleis again initiated whenUser#admin?is called.- However, Rails blocks you from initiating a constant lookup when you are in the context of a stale constant. Rails checks this as of today by
qualified_const_defined?(from_mod.name) && Inflector.constantize(from_mod.name).equal?(from_mod)wherefrom_mod=UserCore.
How can we fix this?
- Don’t try to initiate constant lookup from a stale constant.
- In this case, reinstantiate user.
- Or… Don’t cache any constants in modules and classes which can be reloaded.
- Change
role?(UserRole::ADMIN)torole?(::UserRole::ADMIN). This then will cacheUserRolein the top-level constantObject, and thus will not be affected byUserCorereload.
- Change