Created
May 26, 2017 12:45
-
-
Save denisahearn/f12ae66ac70b3f8651a9239302e81566 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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