From b07f592aaf10e477da686daa9e3efd1f46d67c6a Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 9 Apr 2026 12:33:27 -0400 Subject: [PATCH] Fix broken background image after removal (#2817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ActiveStorage::Record and ApplicationRecord used separate connection pools (from separate `connects_to` calls). This meant `belongs_to touch: true` on attachments silently failed — the card couldn't join the attachment's transaction to receive the deferred touch, so fragment caches were never invalidated after image removal. Fix by delegating `connection_pool` from ActiveStorage::Record to ApplicationRecord so they share a single pool. --- config/initializers/active_storage.rb | 16 ++++++++-------- test/models/storage/no_reuse_test.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index 586c292a1..0062d6979 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -19,15 +19,15 @@ ActiveSupport.on_load(:action_text_content) do end 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 +# ApplicationRecord calls `configure_replica_connections` to set up connection pools for the +# application models. We want ActiveStorage::Record to use the same pools for transactional +# integrity, proper callback invocation, joins, etc., however ActiveStorage::Record inherits from +# ActiveRecord::Base, not ApplicationRecord. This is how we make Active Storage always use the +# ApplicationRecord connection pool. ActiveSupport.on_load(:active_storage_record) do - configure_replica_connections + class << self + delegate :connection_pool, to: "ApplicationRecord" + end end module ActiveStorageControllerExtensions diff --git a/test/models/storage/no_reuse_test.rb b/test/models/storage/no_reuse_test.rb index e876fe50c..1142e2a2e 100644 --- a/test/models/storage/no_reuse_test.rb +++ b/test/models/storage/no_reuse_test.rb @@ -57,6 +57,32 @@ class Storage::NoReuseTest < ActiveSupport::TestCase ).count end + test "purge touches the record to invalidate cache" do + card = @board.cards.create!(title: "Card", creator: users(:david)) + card.image.attach(io: file_fixture("moon.jpg").open, filename: "moon.jpg", content_type: "image/jpeg") + + original_updated_at = card.reload.updated_at + + travel 1.second do + card.image.purge + end + + assert card.reload.updated_at > original_updated_at + end + + test "purge_later touches the record to invalidate cache" do + card = @board.cards.create!(title: "Card", creator: users(:david)) + card.image.attach(io: file_fixture("moon.jpg").open, filename: "moon.jpg", content_type: "image/jpeg") + + original_updated_at = card.reload.updated_at + + travel 1.second do + card.image.purge_later + end + + assert card.reload.updated_at > original_updated_at + end + test "purge_later does not purge blob when still attached elsewhere" do file = file_fixture("moon.jpg") blob = ActiveStorage::Blob.create_and_upload! \