🚗
SWRのrevalidateのタイミング起因で、Railsのrace conditionが起こった話
概要
この事象が起こりました。
- RailsのAPIに対して、sessionの更新を行う趣旨のリクエストを投げた(リクエストA)
- その時点でsessionの内容をログに吐くと、書き込みは成功している
- しかし、次に別のリクエストでsessionにアクセスをすると、そこには書き込んだはずのデータはないと言われる(リクエストB)
なぜ?
結論
Rails側でrace conditionが発生したためでした。
リクエストA, Bの他に、RailsはリクエストAの直後、Cという、一見sessionにはなんの関係もないリクエストも受け付けており、そのCを受け付けたことにより、Aによるsessionの更新内容が失われていた、というのが原因でした。
詳細な話
Rails側
- 値の取得は、そのリクエストで最初にセッションを使用するタイミングに実行される
- 値の書き込みは、Rackミドルウェアのレスポンス処理の中で全てのリクエストにおいて実行される
なぜRails側でrace conditionが起きたのか?については、この記事がとても参考になります。
Railsは上記引用の特徴を持っているのですが、要するに、フロントからRailsにほぼ同時に2つのリクエストが飛び、最初のりクエストで更新したことを、次のリクエストで上書きして、更新がなかったことになっていました。
以下の流れでlost updateが起こったということです。
- RailsがリクエストAを受け取り、その時点のsessionの取得を行う
- リクエストAの内容である、 sessionの更新処理がスタートする
- RailsがリクエストCを受け取り、その時点のsessionの取得を行う(内容は1の時と同じ)
- 2の処理が終了し、レスポンス生成時に、更新した項目を含んだsessionが保存される
- 3の処理が終了し、レスポンス生成時に、更新前の内容のsessionで、4のsessionを上書きで保存する(lost update)
- RailsがリクエストBが受け取り、sessionにアクセスするも4の更新は失われている
フロント側
フロントではSWRでRails APIのfetchを管理していました。
併せて、sessionの更新処理を行うリクエスト(リクエストA)は、SWRのuseSWRMutationで行っていました。
これとuseSWRを使うと、書き込み処理を終えたタイミングで、同一のエンドポイントに対してfetchを行っているuseSWRが、revalidateを行ってくれます。
問題は、この書き込み処理を、リクエストAの前のタイミングで行っており、それに伴うrevalidateのリクエストが、リクエストAの直後に飛んでいたということでした。
なので、上述した6ステップの流れは、より詳細に書くとこうなります。
- フロントが、useSWRMutationを使って、書き込み処理をRailsにリクエストを投げる
- Railsで、1の書き込み処理の内容が完了する
- フロントが、sessionの内容を更新する、リクエストを投げる(リクエストA)
- Railsが、リクエストAを受け取り、その時点のsessionの取得を行う
- Railsで、sessionの更新処理がスタートする
- リクエストAの完了通知がフロントに向かい、それにより、フロントが1のrevalidateであるリクエストを自動でRailsに投げる(リクエストC)
- Railsが、リクエストCを受け取り、その時点のsessionの取得を行う(内容は4の時と同じ)
- 5の処理が終了し、レスポンス生成時に、更新した項目を含んだsessionが保存される
- 7の処理が終了し、レスポンス生成時に、更新前の内容のsessionで、8のsessionを上書きで保存する(lost update)
- RailsがリクエストBが受け取り、sessionにアクセスするも9の更新は失われている
対応
- useSWRMutationのrevalidateオプションをオフにする
- この書き込み処理と同じエンドポイントを見ているuseSWRのmutate APIをリフレッシュの用途で使って、revalidateリクエストのタイミングをコントロールする
- 書き込みリクエスト -> mutate API -> revalidate リクエスト -> リクエストA とする
所感
RailsはGETでも、レスポンス作る時にsessionの書き込みをやるんですね。
参考
Discussion