Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Fixed a bug #2

Open
wants to merge 1 commit into from

1 participant

@perplexes

Hey, I found an infinite loop bug that I've fixed and provided tests for. Please let me know if you have any questions.

@perplexes perplexes Fix infinite loop bug.
Cause: an on_commit callback creates a transaction, which afterwards loops through all the on_commit records waiting to be called, which includes the one that creates a transaction.

Fix: Have a commit object stack that gets pushed and popped for each commit object, so that it doesn't loop, and (untested) its inner-transaction's on_commit can run.
e687184
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 6, 2010
  1. @perplexes

    Fix infinite loop bug.

    perplexes authored
    Cause: an on_commit callback creates a transaction, which afterwards loops through all the on_commit records waiting to be called, which includes the one that creates a transaction.
    
    Fix: Have a commit object stack that gets pushed and popped for each commit object, so that it doesn't loop, and (untested) its inner-transaction's on_commit can run.
This page is out of date. Refresh to see the latest.
View
75 lib/after_commit.rb
@@ -1,33 +1,92 @@
+require 'rubygems'
+require 'ruby-debug'
+Debugger.start
module AfterCommit
+ @@records = {}
+ @@records_queue = []
+
+ @@record_methods = {
+ :all => :committed_records,
+ :create => :committed_records_on_create,
+ :update => :committed_records_on_update,
+ :destroy => :committed_records_on_destroy
+ }
+
+ @@callback_methods = {
+ :all => :after_commit_callback,
+ :create => :after_commit_on_create_callback,
+ :update => :after_commit_on_update_callback,
+ :destroy => :after_commit_on_destroy_callback
+ }
+
+ def self.records
+ @@records
+ end
+
+ # Push a new variable holder onto our stack.
+ def self.push
+ @@records_queue.push @@records
+ @@records = {}
+ end
+
+ def self.pop
+ @@records = @@records_queue.pop
+ end
+
+ # Send callbacks of this type after a commit.
+ # We push a new variable holder on each record, then pop it off, which avoids
+ # an infinite loop whereby an on_commit callback makes a new transaction
+ # (like in creating a BackgrounDRb record)
+ def self.callback(crud_method, &block)
+ record_method = @@record_methods[crud_method]
+ callback_method = @@callback_methods[crud_method]
+ committed_records = send(record_method)
+
+ unless committed_records.empty?
+ committed_records.each do |record|
+ push
+ record.send(callback_method)
+ pop
+ end
+ end
+
+ records[crud_method] = []
+ end
+
+ def self.clear!
+ @@records = {}
+ @@records_queue = []
+ end
+
def self.committed_records
- @@committed_records ||= []
+ records[:all] ||= []
end
def self.committed_records=(committed_records)
- @@committed_records = committed_records
+ records[:all] = committed_records
end
def self.committed_records_on_create
- @@committed_records_on_create ||= []
+ records[:create] ||= []
end
def self.committed_records_on_create=(committed_records)
- @@committed_records_on_create = committed_records
+ records[:create] = committed_records
end
def self.committed_records_on_update
- @@committed_records_on_update ||= []
+ records[:update] ||= []
end
def self.committed_records_on_update=(committed_records)
- @@committed_records_on_update = committed_records
+ records[:update] = committed_records
end
def self.committed_records_on_destroy
- @@committed_records_on_destroy ||= []
+ records[:destroy] ||= []
end
def self.committed_records_on_destroy=(committed_records)
- @@committed_records_on_destroy = committed_records
+ records[:destroy] = committed_records
end
end
View
68 lib/after_commit/connection_adapters.rb
@@ -21,80 +21,26 @@ def commit_db_transaction_with_callback
def rollback_db_transaction_with_callback
rollback_db_transaction_without_callback
- AfterCommit.committed_records = []
- AfterCommit.committed_records_on_create = []
- AfterCommit.committed_records_on_update = []
- AfterCommit.committed_records_on_destroy = []
+ AfterCommit.clear!
end
alias_method_chain :rollback_db_transaction, :callback
- protected
+ protected
+
def trigger_after_commit_callbacks
- # Trigger the after_commit callback for each of the committed
- # records.
- if AfterCommit.committed_records.any?
- AfterCommit.committed_records.each do |record|
- begin
- record.after_commit_callback
- rescue
- end
- end
- end
-
- # Make sure we clear out our list of committed records now that we've
- # triggered the callbacks for each one.
- AfterCommit.committed_records = []
+ AfterCommit.callback(:all)
end
def trigger_after_commit_on_create_callbacks
- # Trigger the after_commit_on_create callback for each of the committed
- # records.
- if AfterCommit.committed_records_on_create.any?
- AfterCommit.committed_records_on_create.each do |record|
- begin
- record.after_commit_on_create_callback
- rescue
- end
- end
- end
-
- # Make sure we clear out our list of committed records now that we've
- # triggered the callbacks for each one.
- AfterCommit.committed_records_on_create = []
+ AfterCommit.callback(:create)
end
def trigger_after_commit_on_update_callbacks
- # Trigger the after_commit_on_update callback for each of the committed
- # records.
- if AfterCommit.committed_records_on_update.any?
- AfterCommit.committed_records_on_update.each do |record|
- begin
- record.after_commit_on_update_callback
- rescue
- end
- end
- end
-
- # Make sure we clear out our list of committed records now that we've
- # triggered the callbacks for each one.
- AfterCommit.committed_records_on_update = []
+ AfterCommit.callback(:update)
end
def trigger_after_commit_on_destroy_callbacks
- # Trigger the after_commit_on_destroy callback for each of the committed
- # records.
- if AfterCommit.committed_records_on_destroy.any?
- AfterCommit.committed_records_on_destroy.each do |record|
- begin
- record.after_commit_on_destroy_callback
- rescue
- end
- end
- end
-
- # Make sure we clear out our list of committed records now that we've
- # triggered the callbacks for each one.
- AfterCommit.committed_records_on_destroy = []
+ AfterCommit.callback(:destroy)
end
#end protected
end
View
33 test/after_commit_test.rb
@@ -8,31 +8,53 @@
ActiveRecord::Base.establish_connection({"adapter" => "sqlite3", "database" => 'test.sqlite3'})
begin
- ActiveRecord::Base.connection.execute("drop table mock_records");
+ ActiveRecord::Base.connection.execute("drop table mock_records")
+ ActiveRecord::Base.connection.execute("drop table mock_non_callbacks")
rescue
end
-ActiveRecord::Base.connection.execute("create table mock_records(id int)");
+ActiveRecord::Base.connection.execute("create table mock_records(id int)")
+ActiveRecord::Base.connection.execute("create table mock_non_callbacks(id int)")
require File.dirname(__FILE__) + '/../init.rb'
+class MockNonCallback < ActiveRecord::Base; end
+
class MockRecord < ActiveRecord::Base
+ attr_accessor :after_commit_called
attr_accessor :after_commit_on_create_called
attr_accessor :after_commit_on_update_called
attr_accessor :after_commit_on_destroy_called
+
+ def clear_flags
+ @after_commit_called = @after_commit_on_create_called = @after_commit_on_update_called = @after_commit_on_destroy_called = nil
+ end
+
+ after_commit :do_commit
+ def do_commit
+ raise "Re-called on commit!" if self.after_commit_called
+ self.after_commit_called = true
+ MockNonCallback.transaction{ MockNonCallback.create! }
+ end
after_commit_on_create :do_create
def do_create
+ raise "Re-called on create!" if self.after_commit_on_create_called
self.after_commit_on_create_called = true
+ MockNonCallback.transaction{ MockNonCallback.create! }
end
after_commit_on_update :do_update
def do_update
+ raise "Re-called on update!" if self.after_commit_on_update_called
self.after_commit_on_update_called = true
+ MockNonCallback.transaction{ MockNonCallback.create! }
end
- after_commit_on_create :do_destroy
+ after_commit_on_destroy :do_destroy
def do_destroy
+ raise "Re-called on destroy!" if self.after_commit_on_destroy_called
self.after_commit_on_destroy_called = true
+ MockNonCallback.transaction{ MockNonCallback.create! }
end
end
@@ -43,11 +65,14 @@ def test_after_commit_on_create_is_called
def test_after_commit_on_update_is_called
record = MockRecord.create!
+ record.clear_flags
record.save
assert_equal true, record.after_commit_on_update_called
end
def test_after_commit_on_destroy_is_called
- assert_equal true, MockRecord.create!.destroy.after_commit_on_destroy_called
+ record = MockRecord.create!
+ record.clear_flags
+ assert_equal true, record.destroy.after_commit_on_destroy_called
end
end
Something went wrong with that request. Please try again.