rails: ActiveStorage contaminating model instances resulting in ActiveRecord::AssociationTypeMismatch
Using ActiveStorage can result in the class instance becoming contaminated in a strange way that results in a ActiveRecord::AssociationTypeMismatch
exception when trying to associate the instance with another ActiveRecord model.
This example may seem a little unconventional as I discovered this issue whilst building seeds for my application.
Steps to reproduce
require 'open-uri'
class User
has_one_attached :avatar
has_many :talks
end
class Talk
belongs_to :user
end
user_a = User.create!({ email: 'user.a@example.com' })
user_b = User.create!({ email: 'user.b@example.com' })
user_a.avatar.attach(io: open('https://example.com/any/valid/image/uri'))
user_a.class.to_s
# => "User"
user_b.class.to_s
# => "User"
Talk.new({ user: user_a })
# => ActiveRecord::AssociationTypeMismatch
Talk.new({ user: user_b })
# => true
Expected behavior
Associating user_a
to a new instance of Talk
should return true
.
Actual behavior
The following exception is unexpectedly raised:
ActiveRecord::AssociationTypeMismatch (User(#70172285884400) expected, got #<User id: "5c01db01-fd4d-464c-bb36-33d29469fe77", email: "example@example.com", created_at: "2019-10-29 21:20:16", updated_at: "2019-10-29 21:20:45"> which is an instance of User(#70172283718280))
Further information
I added a binding.pry
into the method raise_on_type_mismatch!
in activerecord-6.0.0/lib/active_record/associations/association.rb
. I’ve included the output below:
From: /Users/adambutler/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/associations/association.rb @ line 275 ActiveRecord::Associations::Association#raise_on_type_mismatch!:
273: def raise_on_type_mismatch!(record)
274: unless record.is_a?(reflection.klass)
=> 275: binding.pry
276: fresh_class = reflection.class_name.safe_constantize
277: unless fresh_class && record.is_a?(fresh_class)
278: message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, "\
279: "got #{record.inspect} which is an instance of #{record.class}(##{record.class.object_id})"
280: raise ActiveRecord::AssociationTypeMismatch, message
281: end
282: end
283: end
[1] pry(#<ActiveRecord::Associations::BelongsToAssociation>)> record.is_a?(reflection.klass)
=> false
[2] pry(#<ActiveRecord::Associations::BelongsToAssociation>)> record
=> #<User id: "a19c59f1-fd39-40da-abd8-42bc94f2bfc7", email: "user@example.com", created_at: "2019-10-29 21:32:33", updated_at: "2019-10-29 21:32:34">
[3] pry(#<ActiveRecord::Associations::BelongsToAssociation>)> record.class
=> User(id: uuid, email: string)
[4] pry(#<ActiveRecord::Associations::BelongsToAssociation>)> reflection.klass
=> User(id: uuid, email: string)
System configuration
Rails version: 6.0.0
Ruby version: 2.6.5
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 8
- Comments: 15 (6 by maintainers)
Commits related to this issue
- Listen on fork in EventedFileUpdateChecker The Listen gem does not notify across process forks, and so forked processes must re-register their listeners. `EventedFileUpdateChecker` handled this by r... — committed to jonathanhefner/rails by jonathanhefner 4 years ago
- Listen on fork in EventedFileUpdateChecker The Listen gem does not notify across process forks, and so forked processes must re-create their listeners. `EventedFileUpdateChecker` handled this in `up... — committed to jonathanhefner/rails by jonathanhefner 4 years ago
That’s a serious bug 🙄 I’m surprise that issue doesn’t have many 👍
I just ran through the reproduction steps for this issue on the latest commit and I can no longer reproduce it, so I am going to close this issue.
I was not able to reproduce the issue exactly as stated, but I was able to reproduce and investigate something quite similar, involving the interaction between Active Storage, Spring, and the
EventedFileUpdateChecker
.Reproduction steps
has_one_attached :profile_photo
toAccount
config.active_job.queue_adapter = :inline
to config/environments/development.rb(If it is easier I could put this together into a repo and share a link to that.)
Expected behavior
I expected this to print
true
Actual behavior
Account
is getting reloaded in the middle of the example, causing this to returnfalse
What is going on?
EventedFileUpdateChecker
EventedFileUpdateChecker
initializes, it sets@pid
to the current process idEventedFileUpdateChecker
's@pid
will no longer matchProcess.pid
in the forked processAccount
, loading it for the first time.profile_photo
triggers anActiveStorageAnalysis
job.run!
methodcheck!
before running any callbackscheck!
is configured by default to check whether the file watchers have been updatedEventedFileUpdateChecker#updated?
will always return true the first timerun
callbackAccount
constant withActiveSupport::Dependencies.clear
Account
again, it gets loaded for the second time.This came up in https://github.com/thoughtbot/factory_bot/issues/1330 as an
ActiveRecord::AssociationTypeMismatch
error. I closed the issue when I could reproduce it without using factory_bot at all.Workarounds
This bug involves the interaction between spring, active_job, and the evented file update checker. Removing any one of these makes this problem go away:
EventedFileUpdateChecker
in the same process as theupdated?
check.FileUpdateChecker
instead, which doesn’t have this problemVersions
Rails 6.0.3.2 Ruby 2.6.6
I think I understand the problem fairly well at this point, but I am unsure what the best way to go about fixing it is. I would like to chat with somebody more familiar with both the file checker and Rails callbacks, if possible.
I would like to ask that this issue is reopened and the problem can be reproduced on Rails 6.0.2.2