Flaskでファイルを使った排他処理
どうしてもFlask(マルチプロセス)で動作しているプログラムで排他処理をしたくなった。(必要に迫られた)
Linuxなら以下を利用すると良いと思うけれども、Windows環境なので利用できず。
- fcntl --- fcntl および ioctl システムコール https://docs.python.org/ja/3.10/library/fcntl.html
Windowsでファイルを使った履いた処理ってどうするのかな?と思って調べたので、忘れないように記載しておく。
msvcrt
Windowsの場合は以下のモジュールを利用すれば良いらしい
- msvcrt --- MS VC++実行時システムの有用なルーチン群
https://docs.python.org/ja/3.10/library/msvcrt.html?highlight=msvcrt
msvcrt.locking
以下のmsvcrt.lockingを使うことによってファイルのロックの取得とリリースを行うことができる。
オプションがいくつかあって使わないと忘れてしまいそう。
msvcrt.locking(fd, mode, nbytes)
使い方
事前に排他処理を行うために利用するファイルを作成しておく。 その上で
LOCKFILE = open("lockfile.lock", "bw") msvcrt.locking(LOCKFILE.fileno(), msvcrt.LK_NBLCK, 1)
の様にするとファイルのロックを取得できる。
第1引数
ファイルディスクリプタ(fd):ファイルの番号を指定する
第2引数に指定できるオプション
読み込み、書き込みともにロックを掛ける
mode | 機能説明 |
---|---|
msvcrt.LK_LOCK | 指定されたnbytesの範囲にロックを掛ける ロックが取得できるまで1秒間隔で10回リトライを行う 10回リトライを行ってもロックが取得できない場合OSErrorが返る |
msvcrt.LK_NBLCK | 指定されたnbytesの範囲にロックを掛ける ロックが取得できない場合即時にOSErrorが返る |
ファイルの読み込みはできるが、書き込みができない様にロックを掛ける
今回は利用しないので検証していません
mode | 機能説明 |
---|---|
msvcrt.LK_NBRLCK | 指定されたnbytesの範囲にロックを掛ける ロックが取得できるまで1秒間隔で10回リトライを行う 10回リトライを行ってもロックが取得できない場合OSErrorが返る |
msvcrt.LK_RLCK | 指定されたnbytesの範囲にロックを掛ける | ロックを掛けた範囲を読み込むことはできるが書き込むことはできない |
第3引数
ファイルにロックを書ける範囲をバイト数で指定する
サンプルコード
以下のコードでは、2つのエンドポイント(/lockと/unlock)を作成し、ファイルのロックを取得、開放するサンプルです
- /lock:ファイルのロックを取得する
- /unlock:ファイルのロックを開放する
機能
- /lockにアクセスするとファイルのロックを取得することができ、再度/lockにアクセスするとロックに失敗することが確認できます
- /unlockにアクセスした後/lockにアクセスすると/lockが再度取得できることが確認できます
- /lockを取得した後、再度/lockを取得すると失敗しますが、ロックの取得は内部的に1秒間隔で10回挑戦を行いますので、挑戦を行っている間に/unlockにアクセスすると問題なくロックが取得できることが確認できます
import msvcrt import time from flask import Flask, jsonify app = Flask(__name__) # ロックファイルを開く LOCKFILE = open("lockfile.lock", "bw") # ロックリトライの最大回数とインターバル LOCK_RETRY_MAX = 10 LOCK_INTERVAL = 1 # ロックを取得するためのデコレータ def retry(func): def wrapper(*args, **kwargs): for _ in range(LOCK_RETRY_MAX): res = func(*args, **kwargs) if res is True: return res # ロックが取得できなかった場合、インターバルを待って再試行する time.sleep(LOCK_INTERVAL) return False return wrapper # ロックを取得する関数 @retry def acquire_lock() -> bool: try: msvcrt.locking(LOCKFILE.fileno(), msvcrt.LK_NBLCK, 1) # ノンブロッキングロックを取得 return True except OSError: return False # ロックを解放する関数 def release_lock() -> bool: try: msvcrt.locking(LOCKFILE.fileno(), msvcrt.LK_UNLCK, 1) # ロックを解放 return True except OSError: return False # ロックを取得するエンドポイント @app.route("/lock") def acquire_lock_test(): res = acquire_lock() if res is True: return jsonify({"status": "locked"}) else: return jsonify({"status": "lock failed"}) # ロックを解放するエンドポイント @app.route("/unlock") def release_lock_test(): res = release_lock() if res is True: return jsonify({"status": "unlocked"}) else: return jsonify({"status": "unlock failed"}) # アプリケーションの実行 if __name__ == "__main__": app.run()