👋

Rubyによるファイルロックの仕組み - バッチ2重起動を防ぐ

2025/02/05に公開

複数のプロセスやジョブが同時に実行される環境で、同じ処理が重複して走ってしまうとデータの整合性や予期せぬ競合状態が発生する恐れがあります。それを防ぐために有効な手軽な手段が「ファイルロック」です。
本記事ではRuby(Rails)でのファイルロックの仕組みについて調べたことをまとめます。

ファイルロックとは?

ファイルロックとは、特定のファイルに対して「この処理は実行中です」といった状態を設定する仕組みです。
一般的に、ファイルロックには以下の種類があります。

  • 排他ロック
    一度に一つのプロセスのみがロックを取得でき、他のプロセスは解放されるまで待つか、取得に失敗する。

  • 共有ロック
    複数のプロセスが同時に共有ロックを取得でき、主に読み取り専用のアクセス時に使用する。

項目 排他ロック 共有ロック
主な挙動 1プロセスのみロック取得可能。ロックを保持している間、そのプロセスは読み取り/書き込みが可能。他のプロセスはロック取得できない。 複数プロセスが当時にロック取得可能。複数プロセスで読み取り可能。
ユースケース ・バッチ処理2重実行防止
・ファイルの更新
・DB書き込み
・設定ファイルの読み込み
・ログファイル参照

さらに、ロック取得後の挙動として「ブロッキング」と「ノンブロッキング」というモードが存在します。

  • ブロッキング
    他のプロセスがロックを取得している場合、ロックが解放されるまで待機する。そのため、待機中はリソースを占有する。

  • ノンブロッキング
    他のプロセスがロックを取得している場合、待機せず即座に失敗する。そのため、待機が発生せずリソースを占有することはない。

Rubyで排他ロックによるファイルロック

Rubyで、バッチの2重起動を防ぐコードを書いてみます。

まず、Rubyにおいてファイルロックをするにはflockメソッドを使います。以下のように使います。

File.open(lock_file_path, "w") do |f|
  f.flock(File::LOCK_EX | File::LOCK_NB)
end

以下、パラメータです。

  • File::LOCK_EX : 排他ロック
  • File::LOCK_SH : 共有ロック
  • File::LOCK_NB : ノンブロッキングモード
  • File::LOCK_UN : ロック解除。この明示的なアンロック以外に、ファイルのcloseやRubyインタプリタの終了 (プロセスの終了)によっても自動的にロック状態は解除される。

flockメソッドを用いてバッチの2重起動を防ぐ処理は以下のようになります。

# バッチ処理実行
def run_batch
  begin
    with_file_lock do
      # バッチ処理
      execute_batch
    end
  rescue StandardError
    Rails.logger.error "他のプロセスが実行中のため処理をスキップします"
  end
end

# ファイルロックによる排他制御
def with_file_lock
  File.open(lock_file_path, "w") do |f|
    # 排他ロック + ノンブロッキング
    f.flock(File::LOCK_EX | File::LOCK_NB)
    yield
  else
    raise "他のプロセスが起動しています"
  end
end

def lock_file_path
  Rails.root.join("tmp", "lockfile")
end

このコードでは排他ロック+ノンブロッキングを指定しているので、ロックされている場合はそのプロセスは即座にエラーを返し処理を終了します。

f.flock(File::LOCK_EX | File::LOCK_NB)

それぞれの定数の数値は以下です。
File::LOCK_EX : 2
File::LOCK_NB : 4

ビット演算(|)してるのでLOCK_EX|LOCK_NB6になります。

flockメソッドは6を受け取ることがわかりましたが、何をしているのでしょうか。

File#flockメソッドは何をしているのか

Rubyのflockメソッドは、OSのファイルロック機能を呼び出します。OSによりファイルロック機能は異なるので、先ほどのコードでflockメソッドが 6 を受け取ると、OS に応じた適切なファイルロックを設定します。

  • Unix系OSの場合
    Unix系OSの場合、flockメソッドはUnixのflockシステムコールを呼び出します。
    flockシステムコールは、協調ロック(Advisory Lock, アドバイザリロック)を行います。

  • Windows系の場合
    Windowsの場合は、Windows APIのLockFileEx()を呼び出すようです。こちらも協調ロックのようです。

UnixのflockシステムコールとWindowsのLockFileEx()で若干挙動が異なるようですが、この記事ではここまでにします。

強制ロックと協調ロック

協調ロックという言葉が出てきましたが、OSによってロックを強制するかどうかが別れます。強制する場合を強制ロック、しない場合を協調ロックといいます。

  • 強制ロック(Mandatory Lock)
    ファイルがロックされている時、OSが問答無用で他プロセスからの読み書きを待機(ブロック)させる。

  • 協調ロック(Advisory Lock)
    ファイルがロックされている時、OSはロックが取得されたかどうかだけを返す。その結果を見てどう処理するか(待機するか、エラーを返すか等)はプログラム側で明示的に行う。

Unix系は基本的に協調ロックのようです。

ファイルロックのユースケースと適さないケース

排他ロックによるファイルロックは手軽に行えることがわかりましたが、バッチの2重起動を防ぎたい場合は単一サーバでの実行やNFSサーバが使える場合に限りそうです。
複数サーバにまたがるような場合は、Redisやデータベースを用いた分散ロックが必要です。

なお、ファイルロックはサーバ再起動によりロック解除されるので、それが問題になる場合にも適さないですね。

参考

Discussion