From 49c4f2adc6069d8e58f3091a797e9182d85ebbb6 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 8 Dec 2025 14:44:49 -0800 Subject: [PATCH] Fix ActiveStorage FileNotFoundError with immediate variant processing (#2022) When ActiveStorage::Record uses `connects_to` for read replica support, it creates a separate connection pool from ApplicationRecord. This causes `after_commit` callbacks to fire in non-deterministic order - the Attachment's `create_variants` callback can fire before the User model's upload callback completes, resulting in FileNotFoundError. The fix removes replica connection configuration from ActiveStorage::Record so it shares the same connection pool as application models, ensuring proper callback ordering. Also reverts test workarounds that were added to work around this issue, since the root cause is now fixed. See: https://github.com/rails/rails/issues/53694 --- config/initializers/active_storage.rb | 14 ++++++++++--- .../users/avatars_controller_test.rb | 8 ++------ test/controllers/users_controller_test.rb | 5 ++--- .../notification/bundle_mailer_test.rb | 3 +-- test/models/user/avatar_test.rb | 20 +++---------------- 5 files changed, 19 insertions(+), 31 deletions(-) diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index d00bd74bf..4a4a0343d 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -4,9 +4,17 @@ ActiveSupport.on_load(:active_storage_blob) do end end -ActiveSupport.on_load(:active_storage_record) do - configure_replica_connections -end +# Don't configure replica connections for ActiveStorage::Record. +# When ActiveStorage uses `connects_to`, it creates a separate connection pool +# from ApplicationRecord. This causes after_commit callbacks to fire in +# non-deterministic order - the Attachment's create_variants callback can fire +# before the User model's upload callback, causing FileNotFoundError when +# using `process: :immediately` for variants. +# See: https://github.com/rails/rails/issues/53694 +# +# ActiveSupport.on_load(:active_storage_record) do +# configure_replica_connections +# end module ActiveStorageControllerExtensions extend ActiveSupport::Concern diff --git a/test/controllers/users/avatars_controller_test.rb b/test/controllers/users/avatars_controller_test.rb index be4532448..3ded884b8 100644 --- a/test/controllers/users/avatars_controller_test.rb +++ b/test/controllers/users/avatars_controller_test.rb @@ -27,9 +27,7 @@ class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest end test "show own image redirects to the blob url" do - # Create blob separately to ensure file is uploaded before variant processing - blob = ActiveStorage::Blob.create_and_upload!(io: File.open(file_fixture("moon.jpg")), filename: "moon.jpg", content_type: "image/jpeg") - users(:david).avatar.attach(blob) + users(:david).avatar.attach(io: File.open(file_fixture("moon.jpg")), filename: "moon.jpg", content_type: "image/jpeg") assert users(:david).avatar.attached? get user_avatar_path(users(:david)) @@ -38,9 +36,7 @@ class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest end test "show other image redirects to the blob url" do - # Create blob separately to ensure file is uploaded before variant processing - blob = ActiveStorage::Blob.create_and_upload!(io: File.open(file_fixture("moon.jpg")), filename: "moon.jpg", content_type: "image/jpeg") - users(:kevin).avatar.attach(blob) + users(:kevin).avatar.attach(io: File.open(file_fixture("moon.jpg")), filename: "moon.jpg", content_type: "image/jpeg") assert users(:kevin).avatar.attached? get user_avatar_path(users(:kevin)) diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb index d8441d033..385bacaf3 100644 --- a/test/controllers/users_controller_test.rb +++ b/test/controllers/users_controller_test.rb @@ -79,10 +79,9 @@ class UsersControllerTest < ActionDispatch::IntegrationTest test "update with valid avatar" do sign_in_as :kevin - # Create blob separately to ensure file is uploaded before variant processing - blob = ActiveStorage::Blob.create_and_upload!(io: File.open(file_fixture("avatar.png")), filename: "avatar.png", content_type: "image/png") + png_file = fixture_file_upload("avatar.png", "image/png") - put user_path(users(:kevin)), params: { user: { avatar: blob.signed_id } } + put user_path(users(:kevin)), params: { user: { avatar: png_file } } assert_redirected_to user_path(users(:kevin)) assert users(:kevin).reload.avatar.attached? assert_equal "image/png", users(:kevin).avatar.content_type diff --git a/test/mailers/notification/bundle_mailer_test.rb b/test/mailers/notification/bundle_mailer_test.rb index 63647e68a..fbc4274c9 100644 --- a/test/mailers/notification/bundle_mailer_test.rb +++ b/test/mailers/notification/bundle_mailer_test.rb @@ -22,12 +22,11 @@ class Notification::BundleMailerTest < ActionMailer::TestCase end test "renders avatar with external image URL when avatar is attached" do - blob = ActiveStorage::Blob.create_and_upload!( + @user.avatar.attach( io: File.open(Rails.root.join("test", "fixtures", "files", "avatar.png")), filename: "avatar.png", content_type: "image/png" ) - @user.avatar.attach(blob) create_notification(@user) diff --git a/test/models/user/avatar_test.rb b/test/models/user/avatar_test.rb index 58b78969c..ed502d7ab 100644 --- a/test/models/user/avatar_test.rb +++ b/test/models/user/avatar_test.rb @@ -2,8 +2,7 @@ require "test_helper" class User::AvatarTest < ActiveSupport::TestCase test "avatar_thumbnail returns variant for variable images" do - blob = ActiveStorage::Blob.create_and_upload!(io: File.open(file_fixture("moon.jpg")), filename: "moon.jpg", content_type: "image/jpeg") - users(:david).avatar.attach(blob) + users(:david).avatar.attach(io: File.open(file_fixture("moon.jpg")), filename: "moon.jpg", content_type: "image/jpeg") assert users(:david).avatar.variable? assert_equal users(:david).avatar.variant(:thumb).blob, users(:david).avatar_thumbnail.blob @@ -17,8 +16,7 @@ class User::AvatarTest < ActiveSupport::TestCase end test "allows valid image content types" do - blob = ActiveStorage::Blob.create_and_upload!(io: File.open(file_fixture("moon.jpg")), filename: "test.jpg", content_type: "image/jpeg") - users(:david).avatar.attach(blob) + users(:david).avatar.attach(io: File.open(file_fixture("moon.jpg")), filename: "test.jpg", content_type: "image/jpeg") assert users(:david).valid? end @@ -31,19 +29,7 @@ class User::AvatarTest < ActiveSupport::TestCase end test "thumb variant is processed immediately on attachment" do - # Create blob separately to ensure file is uploaded before variant processing. - # - # Root cause: When ActiveStorage::Record uses `connects_to` for read replica support - # (as in SAAS mode), it creates a separate connection pool from application models. - # Since after_commit callbacks are tracked per connection pool, the callback order - # between User's pool (upload) and Attachment's pool (create_variants) isn't guaranteed. - # In MySQL/SAAS mode, the Attachment callback fires before the file is uploaded. - blob = ActiveStorage::Blob.create_and_upload!( - io: File.open(file_fixture("avatar.png")), - filename: "avatar.png", - content_type: "image/png" - ) - users(:david).avatar.attach(blob) + users(:david).avatar.attach(io: File.open(file_fixture("avatar.png")), filename: "avatar.png", content_type: "image/png") assert users(:david).avatar.variant(:thumb).processed? end