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
Module
context. 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 forUserRole
constant 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,UserCore
is reloaded, butu
still has the older version of the constant. Rails raises anArgumentError
with 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 calledUserCore
is replaced with a newer version andUserCore.const_defined?('UserRole')
is no longertrue
. Hence rails autoloading ofUserRole
is 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 cacheUserRole
in the top-level constantObject
, and thus will not be affected byUserCore
reload.
- Change