背景
業務の振り返りとしてここにまとめます。
外部サービスとOAuth認可で連携する機能があり、その際のトークンに関わる処理の見直しをしました。
前提
メインサーバー、イベントをリッスンし実行するワーカーサーバー、バッチ用のサーバーから同一の外部サービスにリクエストを送るため、プロセス内に限らず、マルチプロセスでの並列リクエストが起こり得る設計になっています。
当時の処理
リクエストの度に以下を行っていました。
- リフレッシュトークンでアクセストークンを更新する
- そのアクセストークンで外部サービスにリクエストを投げる
- 更新されたアクセストークンはDBに保持しない
リフレッシュトークンは同じものを使い続けています。
外部サービスの仕様変更による連携の障害
外部サービスの仕様が以下のように変わりました。
- アクセストークン更新時にリフレッシュトークンも更新する
これにより、アプリは更新されたリフレッシュトークンをDBに保持することはしないため、2回目のリクエストで失敗するという障害が起こりました。
対応方針 「期限切れになるまで access_token を使い回す」
理由
- 毎回アクセストークンを更新する運用の場合、直列で各リクエストを行うようにするとロック待機時間が課題となる
- refresh すると既存の access_token を失効させることも分かったため
前提: 毎回refreshする運用では、並列リクエストは成立しない(競合して失敗する)
しかしロックにより直列化すると、ロック待機時間が課題となる
採用案: access_token を使い回す
access_tokenが期限切れの際のrefresh周りで排他制御をする
access_tokenが期限切れの場合はrefreshをします。
その際に並列リクエストが走る場合を考慮します。
先にリクエストを実行する予定だったもの(リクエストA)のaccess_tokenが、後続リクエストによりrefreshされると、リクエストAは失敗します。
そのため、以下のように排他制御が必要になります。
refresh周りの排他制御の技術選定
物理ロック(行ロック)を取得せず、論理ロックを取得することにした理由
- 行ロックだと、その間 トークンをカラムに持つテーブルの同一行を触る無関係なトランザクションがブロックされるため
- 論理ロックは同じキーを取りに来たトークン更新リクエストに対してブロック(直列化)することができる
pg_advisory_xact_lock はDBサーバー側のロックなので、バッチ用のサーバーも、mainのサーバーも同じキーを使うことでマルチプロセス間での排他制御をすることが可能です。
ロックはトランザクション終了時に自動解放されます。
参考
- リクエストを送信するたびにアクセストークンを更新するのはなぜ良くないのか
- なぜリクエストのたびにリフレッシュトークンを送信するのか
- トークン更新時の同時実行処理方法
- OAuthリフレッシュトークンの競合について
- Redisを用いた分散ロック
- Goライブラリ singleflightについて
DB
- データベースの排他制御
- SELECT FOR UPDATEの使い方と注意点
- アドバイザリーロックについて
- 排他ロックとは
- ロック取得順を一か所で決めてデッドロックを避ける