after_transaction_commit gem extraction

refs CNVS-15881

our delayed_job depends on this, so I'm pulling it out into a gem first,
so that we can fully gemify our fork of delayed_job and use it in other
apps.

I modified the code to be built on top of the after_commit callback
functionality added in rails3, and I pulled in the test_after_commit gem
(after making a fix to it), which emulates transaction callbacks in test
mode. This makes it easier to write tests that interacts with these
callbacks, and cleans up some code in the app that was checking for test
mode and changing behavior. Fortunately, it behaves well with once-ler.

Side note: now that we are on rails3, we could replace some, but not
all, of our usage of after_transaction_commit with after_commit AR
callbacks. I didn't do that in this commit, to minimize churn.

test plan: things that happen in commit callbacks should still happen as
expected. for instance, with caching enabled, changing a /plugins
setting should still take effect immediately, because the cache is
cleared in a commit callback.

Change-Id: I66af5f66bf9fa9354352ed37edec8bd92d8e39c8
Reviewed-on: https://gerrit.instructure.com/41819
Reviewed-by: Cody Cutrer <cody@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Product-Review: Brian Palmer <brianp@instructure.com>
This commit is contained in:
Brian Palmer 2014-09-25 17:04:54 -06:00
parent ff129a694d
commit 5f80487ad9
7 changed files with 15 additions and 134 deletions

View File

@ -24,6 +24,7 @@ gem 'switchman', '1.2.21'
gem 'folio-pagination', '0.0.7', :require => 'folio/rails'
gem 'will_paginate', '3.0.4', :require => false
gem "after_transaction_commit", '1.0.1'
gem "aws-sdk", '1.52.0'
gem 'uuidtools', '2.1.4'
gem 'barby', '0.5.0'

View File

@ -25,6 +25,7 @@ group :test do
gem 'selenium-webdriver', '2.43.0'
gem 'childprocess', '0.5.0'
gem 'websocket', '1.0.7'
gem 'test_after_commit', '0.3.0', github: 'codekitchen/test_after_commit', branch: 'nested-commit-callbacks'
gem 'webmock', '1.16.1', :require => false
gem 'addressable', '2.3.5'
gem 'crack', '0.4.1'

View File

@ -940,69 +940,12 @@ class ActiveRecord::ConnectionAdapters::AbstractAdapter
col
}
end
def after_transaction_commit(&block)
if open_transactions <= base_transaction_level
block.call
else
@after_transaction_commit ||= []
@after_transaction_commit << block
end
end
def after_transaction_commit_callbacks
@after_transaction_commit || []
end
# the alias_method_chain needs to happen in the subclass, since they all
# override commit_db_transaction
def commit_db_transaction_with_callbacks
commit_db_transaction_without_callbacks
run_transaction_commit_callbacks
end
# this will only be chained in in Rails.env.test?, but we still
# sometimes stub Rails.env.test? in specs to specifically
# test behavior like this, so leave the check in this code
def release_savepoint_with_callbacks
release_savepoint_without_callbacks
return unless Rails.env.test?
return if open_transactions > base_transaction_level
run_transaction_commit_callbacks
end
def base_transaction_level
return 0 unless Rails.env.test?
return 1 unless defined?(Onceler)
Onceler.base_transactions
end
def rollback_db_transaction_with_callbacks
rollback_db_transaction_without_callbacks
@after_transaction_commit = [] if @after_transaction_commit
end
def run_transaction_commit_callbacks
return unless @after_transaction_commit.present?
# the callback could trigger a new transaction on this connection,
# and leaving the callbacks in @after_transaction_commit could put us in an
# infinite loop.
# so we store off the callbacks to a local var here.
callbacks = @after_transaction_commit
@after_transaction_commit = []
callbacks.each { |cb| cb.call() }
ensure
@after_transaction_commit = [] if @after_transaction_commit
end
end
module MySQLAdapterExtensions
def self.included(klass)
klass::NATIVE_DATABASE_TYPES[:primary_key] = "bigint DEFAULT NULL auto_increment PRIMARY KEY".freeze
klass.alias_method_chain :configure_connection, :pg_compat
klass.alias_method_chain :commit_db_transaction, :callbacks
klass.alias_method_chain :rollback_db_transaction, :callbacks
klass.alias_method_chain :release_savepoint, :callbacks if Rails.env.test?
end
def rename_index(table_name, old_name, new_name)
@ -1212,14 +1155,6 @@ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
end
end
if defined?(ActiveRecord::ConnectionAdapters::SQLiteAdapter)
ActiveRecord::ConnectionAdapters::SQLiteAdapter.class_eval do
alias_method_chain :commit_db_transaction, :callbacks
alias_method_chain :rollback_db_transaction, :callbacks
alias_method_chain :release_savepoint, :callbacks if Rails.env.test?
end
end
if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
def func(name, *args)
@ -1278,10 +1213,6 @@ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, [], orders)
end
end
alias_method_chain :commit_db_transaction, :callbacks
alias_method_chain :rollback_db_transaction, :callbacks
alias_method_chain :release_savepoint, :callbacks if Rails.env.test?
end
end

View File

@ -19,18 +19,12 @@ module Delayed
# transaction are visible
connection = self.connection if respond_to?(:connection)
connection ||= ActiveRecord::Base.connection
transactions = 0
# specs run in an outer transaction that needs to be ignored
transactions += 1 if Rails.env.test?
if connection.open_transactions > transactions
if !Rails.env.test? && (Delayed::Job != Delayed::Backend::ActiveRecord::Job ||
connection != Delayed::Job.connection)
connection.after_transaction_commit do
Delayed::Job.enqueue(Delayed::PerformableMethod.new(self, method.to_sym, args), enqueue_args)
end
return nil
if (Delayed::Job != Delayed::Backend::ActiveRecord::Job || connection != Delayed::Job.connection)
connection.after_transaction_commit do
Delayed::Job.enqueue(Delayed::PerformableMethod.new(self, method.to_sym, args), enqueue_args)
end
return nil
end
end

View File

@ -71,12 +71,12 @@ describe 'Delayed::Backend::Redis::Job' do
describe "send_later" do
it "should schedule job on transaction commit" do
Rails.env.stubs(:test?).returns(false)
before_count = Delayed::Job.jobs_count(:current)
job = "string".send_later :reverse
expect(job).to be_nil
expect(Delayed::Job.jobs_count(:current)).to eq before_count
ActiveRecord::Base.connection.run_transaction_commit_callbacks
User.transaction do
job = "string".send_later :reverse
expect(job).to be_nil
expect(Delayed::Job.jobs_count(:current)).to eq before_count
end
Delayed::Job.jobs_count(:current) == before_count + 1
end
end

View File

@ -428,52 +428,6 @@ describe ActiveRecord::Base do
end
end
context "after_transaction_commit" do
self.use_transactional_fixtures = false
before do
Rails.env.stubs(:test?).returns(false)
end
it "should execute the callback immediately if not in a transaction" do
a = 0
User.connection.after_transaction_commit { a += 1 }
expect(a).to eq 1
end
it "should execute the callback after commit if in a transaction" do
a = 0
User.connection.transaction do
User.connection.after_transaction_commit { a += 1 }
expect(a).to eq 0
end
expect(a).to eq 1
end
it "should not execute the callbacks on rollback" do
a = 0
User.connection.transaction do
User.connection.after_transaction_commit { a += 1 }
expect(a).to eq 0
raise ActiveRecord::Rollback
end
expect(a).to eq 0
User.connection.transaction do
# verify that the callback gets cleared out, so this second transaction won't trigger it
end
expect(a).to eq 0
end
it "should avoid loops due to callbacks causing a new transaction" do
a = 0
User.connection.transaction do
User.connection.after_transaction_commit { User.connection.transaction { a += 1 } }
expect(a).to eq 0
end
expect(a).to eq 1
end
end
context "Finder tests" do
before :once do
@user = user_model

View File

@ -466,14 +466,14 @@ describe ContentTag do
end
it "should touch the module after committing the save" do
Rails.env.stubs(:test?).returns(false)
course
mod = @course.context_modules.create!
yesterday = 1.day.ago
ContextModule.where(:id => mod).update_all(:updated_at => yesterday)
tag = mod.add_item :type => 'context_module_sub_header', :title => 'blah'
expect(mod.reload.updated_at.to_i).to eq yesterday.to_i
mod.connection.run_transaction_commit_callbacks
ContextModule.transaction do
tag = mod.add_item :type => 'context_module_sub_header', :title => 'blah'
expect(mod.reload.updated_at.to_i).to eq yesterday.to_i
end
expect(mod.reload.updated_at).to be > 5.seconds.ago
end