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:
David Taylor 2020-12-08 00:03:31 +00:00 committed by GitHub
parent 76b04afca3
commit ed91385c18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 51 additions and 21 deletions

View File

@ -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)

View File

@ -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

View File

@ -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