Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save denisahearn/f12ae66ac70b3f8651a9239302e81566 to your computer and use it in GitHub Desktop.
Save denisahearn/f12ae66ac70b3f8651a9239302e81566 to your computer and use it in GitHub Desktop.
# This class wraps an ActiveRecord database transaction, and contains a workaround
# for a memory bloat issue that happens when you create a large number of ActiveRecord
# model instances within a database transaction, and those model classes use the
# paper_trail gem for versioning.
#
# This workaround temporarily removes transaction callbacks registered on ActiveRecord
# classes by paper_trail, and then manually calls these callbacks once the transaction
# has finished. The precense of the transaction callbacks is what causes ActiveRecord
# to hold onto the model instances until the transaction finishes. ActiveRecord will
# not hold onto the model instances if no transaction callbacks are defined.
#
# Usage:
#
# DatabaseTransaction.new.perform do
# # Your code that creates a large number of ActiveRecord models
# end
#
# See https://github.com/airblade/paper_trail/issues/962 for more details.
class DatabaseTransaction
def initialize
@callbacks = {}
end
def perform
raise ArgumentError, "Missing block" unless block_given?
begin
clear_transaction_callbacks
transaction_committed = false
ApplicationRecord.transaction do
begin
yield
transaction_committed = true
rescue ActiveRecord::Rollback => ex
transaction_committed = false
raise ex
end
end
if transaction_committed
run_transaction_callbacks(:commit)
else
run_transaction_callbacks(:rollback)
end
ensure
restore_transaction_callbacks
end
end
private
def clear_transaction_callbacks
model_classes.each do |model_class|
# First capture the transaction callbacks for this model class
# so they can be restored later on
@callbacks[model_class] = {
commit: model_class._commit_callbacks.dup,
rollback: model_class._rollback_callbacks.dup
}
# Now clear the transaction callbacks for this model class
model_class._commit_callbacks.clear
model_class._rollback_callbacks.clear
end
end
def restore_transaction_callbacks
if @callbacks.any?
model_classes.each do |model_class|
# Restore the commit callbacks for this model class
commit_callbacks = @callbacks[model_class][:commit]
commit_callbacks.each {|callback| model_class._commit_callbacks.append(callback)}
# Restore the rollback callbacks for this model class
rollback_callbacks = @callbacks[model_class][:rollback]
rollback_callbacks.each {|callback| model_class._rollback_callbacks.append(callback)}
end
end
end
def run_transaction_callbacks(type)
if @callbacks.any?
model_classes.each do |model_class|
transaction_callbacks = @callbacks[model_class][type]
transaction_callbacks.compile
end
end
end
def model_classes
@model_classes ||=
Dir.glob('app/models/*.rb').map do |file|
class_name = file[/app\/models\/(.*)\.rb/, 1].camelize
Object.const_get(class_name)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment