Lost in Transaction

November 4, 2010
rails3 callbacks transactions

I am working on an issue tracking system at work. I built in an API so other systems can create events, but I wanted to make sure to minimize duplicate events, and opted to just add a comment in case the same issue was opened twice (or more.)

I started with this test

context "don't allow duplicate issues" do
  setup do
    @issue = Issue.create(:name => "something", :start_at => "1/1/2010")
  end
  
  should "not create a duplicate issue" do
    assert_no_difference('Issue.count') do
      another_issue = Issue.create(:name => "something", :start_at => "1/1/2010")
    end
  end
  
  should "create a new issue if the duplicate issue is closed" do
    @issue.update_attributes(:resolution => "something")
    assert_difference('Issue.count') do
      another_issue = Issue.create(:name => "something", :start_at => "1/1/2010")
    end
  end
  
  should "add a comment on existing issue" do
    assert_difference('@issue.comments.count') do
      another_issue = Issue.create(:name => "something", :start_at => "1/1/2010")
    end
  end
end


Then wrote this code:

class Issue < ActiveRecord::Base

  has_many :comments
  validate :check_for_open_duplicate, :on => :create

  scope :open, where(:resolution => nil)

  protected
  
  def check_for_open_duplicate    
    open_issue = Issue.open.where(:name => name, :description => description).first
    if open_issue
      open_issue.comments.create(:body => "Duplicate attempted to open")
      errors.add(:duplicate, "Duplicate Issue")
    end
  end
  
end


But it kept failing, which was weird to me, because it seemed right.. it wasn't until I tried it from the development environment and checked out the logs that I saw what was happening..

SQL (0.1ms)  BEGIN
Issue Load (0.1ms)  SELECT `issues`.* FROM `issues` WHERE (`issues`.`resolution` IS NULL) AND (`issues`.`description` = '') AND (`issues`.`name` = 'something') ORDER BY start_at DESC LIMIT 1
SQL (0.2ms)  SELECT COUNT(*) AS count_id FROM `comments`
SQL (18.6ms)  INSERT INTO `comments` (`body`, `commentable_id`, `commentable_type`, `created_at`, `updated_at`, `user_id`) VALUES ('Duplicate attempted to open', 3370, 'Issue', '2010-11-03 14:36:12', '2010-11-03 14:36:12', 1)
SQL (47.5ms)  ROLLBACK


The whole request was being wrapped in a transaction and being rolled back.. A little google searching later, I found after_rollback, and wrote a new callback

class Issue < ActiveRecord::Base

  attr_accessor :duplicate_issue

  has_many :comments
  validate :check_for_open_duplicate, :on => :create
  after_rollback :add_duplicate_comment

  scope :open, where(:resolution => nil)

  protected
  
  def check_for_open_duplicate    
    open_issue = Issue.open.where(:name => name, :description => description).first
    if open_issue
      @duplicate_issue = open_issue
      errors.add(:duplicate, "Duplicate Issue")
    end
  end
  
  def add_duplicate_comment
    @duplicate_issue.comments.create(:body => "Duplicate attempted to open") if @duplicate_issue
  end


end


And now everything is working as intended. So if you run into a similar situation, I hope that this will help out.