RailsでSQLite3を複数Thread/Processから同時WriteするときにBusyExceptionを回避


TL;DR

RailsのDBにSQLite3を使ってしまうと,Redis等々で同時WriteするとSQLite3::BusyExceptionが発生する.
database.ymltimeoutも効かぬらしい.
仕方がないので自分でプロセス横断の排他Lockを実装する.

やったこと

ActiveRecord.save/.save!でfalseが返ったり例外発生したりするので,成功するまでRetryするとかしても良かったかもしれませんが,あまり格好良くないので,排他Lock制御します.

Rubyの排他Lockの実装自体はぐぐるといっぱい出てくるので,それらを参考に↓のようなblockを排他実行させるClassを用意しました.

LockBlock.rb
require 'tmpdir'

class LockBlock
  class << self

    def locked(lock_file_name)
      File::open(File::join(Dir::tmpdir, lock_file_name), 'w') { |file|
        begin
          file.flock(File::LOCK_EX)
          yield
        ensure
          file.flock(File::LOCK_UN)
        end
      }
    end

  end
end

この排他Lockで守った状態でActiveRecord.save/.save!するためのMethodをApplicationRecordに用意します.

app/models/application_record.rb
def safe_save!
  LockBlock::locked(`db_lock`) {
    self.save!
  }
end

あとは各Modelの.save/.save!やってるところをsafe_save!に置き換えれば完了.

これでいいのか疑問

ひとまず ↑ の対応を入れてRedis使って平行動作させて様子見してますが,FileのLock/Unlockのアトミック性とかよくわかっておらず,これで100%保証されてるのかどうかが不明...

# そもそも`SQLite3やめいとかは言わないように...
# 最初はシンプル機能のお手軽Appになるはずやったんや...

---///