アトミックな操作をしたいときのロック状態の作り方

Crontabで実行しているバッチで重複した実行をしてほしくないときにロック状態を作ったりしますが、どうやるのがいいのかの選択肢を整理。

令和の時代と思えない話だけど今も必要な話だった。

おおむね、サイボウズのsatさんの書いていることを確認した感じになります。 https://zenn.dev/satoru_takeuchi/articles/e0636407a0040c

選択肢は以下。

おすすめは一番最後のファイルロック。

汎用性のある話なので、Linuxのコマンドで例を書いています。高等言語でもたいていラッパーがあるし。

ロックファイルを作る

if test -e /var/lock/foobar
then
  exit
fi
touch /var/lock/foobar

# do something

rm /var/lock/foobar

最初でファイルの有無を確認し、なければ作成して続行。処理の最後で削除する。プリミティブ。

単純だけどあんまりおすすめはしない。

  • Pros
    • 単純
  • Cons
    • 確認と作成に時間差があるので、その隙間で重複実行されることがあるかもしれない。crontabからの実行とかなら問題となりにくいけど
    • プロセスが途中で死ぬとロックファイルが残ってしまう
    • ロックファイル名が衝突する可能性がある

シンボリックリンクを作る

ln -s $0 /var/lock/foobaz

# do something

unlink /var/lock/foobaz

ln -s は既に存在していたらexitcode 1を返す。なので、存在確認と作成を同時にできるので、時間差がない。

  • Pros
    • 時間差がないので、隙間で重複実行のリスクがない(はず)
  • Cons
    • プロセスが途中で死ぬとロックファイルが残ってしまう
    • ロックファイル名が衝突する可能性がある

pidofコマンドでプロセスがあるかを調べる

/usr/sbin/pidof -x cron_job.sh >/dev/null || /path/to/your/cron_job.sh

一番簡単かもcron起動プロセスの多重起動防止ワンライナー - Qiita

ワンライナーでやるならこれがいいかも。だけど、プロセスを探すときのクエリーを間違えそう。引数が多い時とか特に。

  • Pros
    • 解除処理が不要
    • プロセスが途中で死んでも問題ない
    • ファイルに頼らないので、ファイル名の衝突などのリスクがない
  • Cons
    • プロセスを探すときのクエリーを間違えそう

実行スクリプト自身をファイルロックする

flock -xn foobar.rb vi foobar.rb

Exclusive, non-blockingのオプションをつければ、実行プロセスは一つにして、他のプロセスは待たせないですぐ完了にできる。これが一番いいと思う。

  • Pros
    • 解除処理が不要
    • プロセスが途中で死んでも問題ない
    • ファイルに頼らないので、ファイル名の衝突などのリスクがない
    • スクリプト自身をロックすれば依存するものを減らせる
  • Cons
    • ないと思う

Rubyでの実装例

以下、Rubyでの実装例

def main
  puts "Start #{Time.now}"
  locked = lock_myself
  if locked
    puts "Grab lock, Stay running"
  else
    puts "Quit due to process duplicated"
    return
  end

  sleep_sec = 65 
  puts "sleep #{sleep_sec} sec"
  sleep sleep_sec
  puts "Completed"
end

def lock_myself
  f = File.open(__FILE__, "a")
  # ロックが取れない場合、falseを返す
  f.flock(File::LOCK_EX | File::LOCK_NB)
end

main

こんなスクリプトが以下のようにcrontabに登録されていると、

ubuntu@ip-172-31-10-98:~$ crontab -l
* * * * * ruby /home/ubuntu/flock_test.rb  >> /var/log/cron_log

以下のように出力される。

出力の順番がずれているのは、スクリプトの完了時に標準出力がフラッシュされるため。ロックが取れないケースの方が先に完了するので先に出る。

ubuntu@ip-172-31-10-98:~$ tail -f  /var/log/cron_log 
Start 2022-10-17 04:09:01 +0000
Quit due to process duplicated
Start 2022-10-17 04:08:01 +0000
Grab lock, Stay running
sleep 65 sec
Completed
Start 2022-10-17 04:11:01 +0000
Quit due to process duplicated
Start 2022-10-17 04:10:01 +0000
Grab lock, Stay running
sleep 65 sec
Completed
Start 2022-10-17 04:13:01 +0000
Quit due to process duplicated
Start 2022-10-17 04:12:01 +0000
Grab lock, Stay running
sleep 65 sec
Completed
Start 2022-10-17 04:15:01 +0000
Quit due to process duplicated
Start 2022-10-17 04:14:01 +0000
Grab lock, Stay running
sleep 65 sec
Completed