この記事では、Python + FastAPIを使用した打刻機能のstartボタン・stopボタンの二重押し問題をflagで防ぐ実装についての記録と実際の実装方法について解説しています。
問題の背景
打刻機能を実装する際、以下の2つの誤操作が発生しうると考えました。
- startボタンを2回連続で押した場合
- stopボタンを2回連続で押した場合
startの二重押しが発生すると、開始時刻が上書きされ記録が壊れます。stopの二重押しが発生すると、終了時刻のみが上書きされ合計時間の計算が崩れます。
フロントエンドだけでバリデーションを行う方法もありますが、今回はバックエンドにも同じチェックを置く設計を採用しました。
バックエンドにもバリデーションを置いた理由
フロントエンドだけにバリデーションを置いた場合、APIを直接叩かれると同じ問題が発生します。
今回のツールはFastAPIでエンドポイントを公開しているため、ブラウザのUIを経由せずにAPIを直接呼び出すことが可能です。この場合、フロントのバリデーションをすり抜けてしまいます。
バックエンドにも同じチェックを置くことで、どこから操作されても同じ動作を保証できます。
処理の全体像は以下の通りです。
フロントとバックエンドの両層でflagを確認し、いずれかが不正な状態であれば処理を止める設計です。
解決方法
flagでボタンの状態を記憶し、これを元にチェックを行う設計にしました。
class TimeCheker():
def __init__(self):
self.flag = False
def start_checker(self):
if self.flag is True:
return "not end"
else:
start_time = datetime.now()
self.start = start_time
self.flag = True
return_time = datetime.now().strftime('%H:%M:%S')
return 'start time is: ' + return_time
def end_checker(self):
if self.flag is False:
return "not start"
else:
self.endtime = datetime.now()
self.total = self.endtime - self.start
result = self.save_to_csv()
self.flag = False
return result
flagがFalseの状態でstartボタンが押されると、時刻を記録してflagをTrueにします。flagがTrueのまま再度startボタンが押された場合はエラーを返し、時刻の上書きを防ぎます。stopボタンも同様の逆判定を行っています。
flagは__init__でFalseに初期化しているため、インスタンス生成時は必ずstart待ちの状態から始まります。
状態の永続保持について
このバリデーションが機能するには、startとstopで同一のflagを参照する必要があります。リクエストごとにインスタンスを生成する設計では、startでTrueにしたflagがstopのリクエスト時には別インスタンスのFalseを参照してしまいます。
このためFastAPIの実装では、TimeChekerのインスタンスをモジュールレベルで1度だけ生成し、複数のリクエストをまたいで同一インスタンスを参照する設計にしています。
# main.py
timer = TimeCheker()
@app.get("/timer/start")
def timer_start():
result = timer.start_checker()
return result
@app.post("/timer/stop")
def timer_end():
result = timer.end_checker()
return result
goalsのような機能ではリクエストごとにインスタンスを生成していますが、timerは状態を保持する必要があるため永続インスタンスとして管理しています。
まとめ
flagによる二重押し防止はシンプルな実装ですが、 インスタンスはモジュールレベルで永続化することが重要になります。(startとstopで同一のflagを参照するため)
モジュールレベルでの永続化が欠けるとバリデーションが意図した通りに機能しません。
リポジトリ
