DEV: Update `DB.after_commit` to be compatible with 'real' transactions (#11294)
Previously it matched the behavior of standard ActiveRecord after_commit callbacks. They do not work well within `joinable: false` nested transactions. Now `DB.after_commit` callbacks will only be run when the outermost transaction has been committed. Tests always run inside transactions, so this also introduces some logic to run callbacks once the test-wrapping transaction is reached.
This commit is contained in:
parent
76b04afca3
commit
ed91385c18
|
@ -32,7 +32,12 @@ class MiniSqlMultisiteConnection < MiniSql::Postgres::Connection
|
|||
end
|
||||
|
||||
def committed!(*)
|
||||
@callback.call
|
||||
if DB.transaction_open?
|
||||
# Nested transaction. Pass the callback to the parent
|
||||
ActiveRecord::Base.connection.add_transaction_record(self)
|
||||
else
|
||||
@callback.call
|
||||
end
|
||||
end
|
||||
|
||||
def before_committed!(*); end
|
||||
|
@ -42,16 +47,25 @@ class MiniSqlMultisiteConnection < MiniSql::Postgres::Connection
|
|||
end
|
||||
end
|
||||
|
||||
def transaction_open?
|
||||
ActiveRecord::Base.connection.transaction_open?
|
||||
end
|
||||
|
||||
if Rails.env.test?
|
||||
def test_transaction=(transaction)
|
||||
@test_transaction = transaction
|
||||
end
|
||||
|
||||
def transaction_open?
|
||||
ActiveRecord::Base.connection.current_transaction != @test_transaction
|
||||
end
|
||||
end
|
||||
|
||||
# Allows running arbitrary code after the current transaction has been committed.
|
||||
# Works with nested ActiveRecord transaction blocks. Useful for scheduling sidekiq jobs.
|
||||
# If not currently in a transaction, will execute immediately
|
||||
def after_commit(&blk)
|
||||
return blk.call if !ActiveRecord::Base.connection.transaction_open?
|
||||
|
||||
# In tests, everything is run inside a transaction.
|
||||
# To run immediately, check for joinable? transaction
|
||||
# This mimics core rails behavior: https://github.com/rails/rails/blob/348e142b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L211
|
||||
return blk.call if Rails.env.test? && !ActiveRecord::Base.connection.current_transaction.joinable?
|
||||
return blk.call if !transaction_open?
|
||||
|
||||
ActiveRecord::Base.connection.add_transaction_record(
|
||||
AfterCommitWrapper.new(&blk)
|
||||
|
|
|
@ -5,27 +5,38 @@ require 'rails_helper'
|
|||
describe MiniSqlMultisiteConnection do
|
||||
|
||||
describe "after_commit" do
|
||||
it "runs callbacks after outermost transaction is committed" do
|
||||
it "works for 'fake' (joinable) transactions" do
|
||||
outputString = "1"
|
||||
|
||||
# Main transaction
|
||||
ActiveRecord::Base.transaction do
|
||||
outputString += "2"
|
||||
DB.exec("SELECT 1")
|
||||
ActiveRecord::Base.transaction do
|
||||
DB.exec("SELECT 2")
|
||||
outputString += "3"
|
||||
DB.after_commit { outputString += "6" }
|
||||
outputString += "4"
|
||||
end
|
||||
DB.after_commit { outputString += "7" }
|
||||
outputString += "5"
|
||||
end
|
||||
|
||||
# Nested transaction
|
||||
ActiveRecord::Base.transaction do
|
||||
expect(outputString).to eq("1234567")
|
||||
end
|
||||
|
||||
it "works for real (non-joinable) transactions" do
|
||||
outputString = "1"
|
||||
|
||||
ActiveRecord::Base.transaction(requires_new: true, joinable: false) do
|
||||
outputString += "2"
|
||||
DB.exec("SELECT 1")
|
||||
ActiveRecord::Base.transaction(requires_new: true) do
|
||||
DB.exec("SELECT 2")
|
||||
outputString += "3"
|
||||
|
||||
DB.after_commit do
|
||||
outputString += "6"
|
||||
end
|
||||
outputString += "4"
|
||||
DB.after_commit { outputString += "6" }
|
||||
outputString += "4"
|
||||
end
|
||||
|
||||
DB.after_commit do
|
||||
outputString += "7"
|
||||
end
|
||||
|
||||
DB.after_commit { outputString += "7" }
|
||||
outputString += "5"
|
||||
end
|
||||
|
||||
|
|
|
@ -273,6 +273,11 @@ RSpec.configure do |config|
|
|||
expect(before_event_count).to eq(after_event_count), "DiscourseEvent registrations were not cleaned up"
|
||||
end
|
||||
|
||||
config.before :each do
|
||||
# This allows DB.transaction_open? to work in tests. See lib/mini_sql_multisite_connection.rb
|
||||
DB.test_transaction = ActiveRecord::Base.connection.current_transaction
|
||||
end
|
||||
|
||||
config.before(:each, type: :multisite) do
|
||||
Rails.configuration.multisite = true # rubocop:disable Discourse/NoDirectMultisiteManipulation
|
||||
|
||||
|
|
Loading…
Reference in New Issue