This post pertains to MySQL with InnoDB.

With heavy concurrency it is common to see the “Lock wait timeout” error on some database transactions. This is a temporary error that can often be made to go away just by trying the transaction again. Here is a little helper method you can put in a Rails initializer, for instance, to give your model a concise way to retry a few times.

class ActiveRecord::Base

  def self.retry_mysql_error(tries=3, &b)
    count = 0
    begin
      count += 1
      return b.call
    rescue Mysql::Error
      if count < tries
        sleep(0.1 * count)
        retry
      end
      raise
    end
  end

end

Just pass it a block (and an optional number of tries) and it should help to reduce the number of timeouts you see. Be careful not to carry over inadvertently any assumptions across retries.

class Download < ActiveRecord::Base

  def Download.increment_download_count!(filename)
    Download.retry_mysql_error(5) do
      record = Download.find_by_filename(filename)
      record.download_count += 1
      record.save!
    end
  end

end

It also plays nice with RSpec.

describe Download do

  describe "increment_download_count!" do

    it "retries mysql error five times" do
      Download.should_receive(:find_by_filename).exactly(5).times.and_raise(Mysql::Error)
      lambda { Download.increment_download_count!(true) }.should raise_error(Mysql::Error)
    end

  end

end

Of course, this technique should be balanced against other considerations. If it is unable to complete successfully after all the retries, it will re-raise the Mysql::Error. If this happens often, you might need to make deeper changes to your application.