【Vue.js】同時にリクエストを送る罠にはまる(厳しいAPI制限回避策)

Vue.js logo
あるサービスとの連携部分を実装していてどうしても上手く動かないのでなんでかなと思ったらVue.jsの実装に問題があったっていう話






どんなことが起きていたか?

あるサービスが提供するAPIに接続してデータを取得したり登録したりするプログラムを実装していて、4件以上の情報を取得しようとするとエラーになって3件までしか取得出来ませんでした。APIの仕様を確認すると、


同時接続可能数:3


となっていて、いやいやそれさすがに制限厳しすぎない?と思って「もうちょっと緩和してくださいよ」と問い合わせたら、「設定に○万円、接続可能数1件増やすごとに月額○千円」という返答が返ってきて、さすがにアコギ過ぎるので実装で何とかすることに。こっちは仕様書とサンプルに沿って実装してるのになあ。



APIリクエストの合間にsleep時間を設ける

同時じゃなければ良いんでしょということで、こういう場合の常套手段としてリクエストの合間にsleep時間を設けることにしました。API自体はPHPフレームワークで送っているんですが、そのPHPフレームワークへのリクエストはVue.jsとaxiosが行っているので、そのVue.jsの部分にsleepを差し込むことに。参考にしたのはこちらで、リクエストごとに1秒の休止期間を設けることにしました。


サーバ側の更新をブラウザ側で検知するため、数秒ごとにリクエストするループを書きました。 また、取得したデータから条件によってそのループを抜ける処理も追加します。

async/awaitとライブラリのaxiosを利用します。

また、同様の処理はWebhookを調べて利用された方がよさそうです。

axiosで数秒ごとにリクエストするループとループを抜ける処理 | JavaScript – suzu6の技術ブログ


Vue.jsで実装するには多少のコツが必要ですが、親子関係とか少し工夫してやればいいだけなのでそれほど難しくはありません。これで処理ごとに1秒(1,000ミリ秒)休む処理が書けました。


render async function() {
 await axios.post() {
...
 }
 await this.sleep(1000)
},
sleep: function(time) {
 return new Promise((resolve, reject) => {
   setTimeout(() => {
     resolve()
   }, time)
 })
}



……ダメでした。



同時にリクエストを送る「罠」

1秒休ませる処理に問題があったわけではありません。確かにasync ~ awaitを使うことで、awaitが終わるのを待ってから次の処理が行われるようになり、確かに1秒間待ってくれるようになります。でもダメでした。ちなみに言うとAPIへのリクエストを処理しているPHPの方でsleepを設定してもダメでした。処理は遅くなるけれど同時接続数は減らない。なんだ?


答えは、テンプレートの実装方法にありました。


問題のテンプレートをものすごく簡略化したのがこちらです。


<div id="items">
 <item_detail
   v-for="item_id in item_ids"
   :key="item_id"
   :id="item_id"
 ></item_detail>
</div>


ループの中でコンポーネント「item_detail」が呼び出されています。で、先ほどのAPIリクエストは「item_detail」コンポーネントの mounted で呼び出されています。ということはAPIへのリクエストは1つずつ順番にされているのではなくて、ループの回転速度に合わせてほぼ同時にされているのです!


動作イメージはこんな感じ。






こりゃあかんわ。



対策:sleepの時間を可変にして対応

sleepをAPIリクエストの前に置き、ループの回数に応じてsleepの時間を延ばすようにしてみました。


イメージはこんな感じです。





render async function() {
 await axios.post() {
...
 }
 await this.sleep(1000 * this.$parent.sleep_time)
},
sleep: function(time) {
 this.$parent.sleep_time = this.$parent.sleep_time + 1
 return new Promise((resolve, reject) => {
   setTimeout(() => {
     resolve()
   }, time)
 })
}


これならばループの中でもうまくAPIリクエストをずらせました。


ただし:実際には調整が必要

イメージ図ではわかりやすさのためにsleep時間を1秒ずつ伸ばしていますが、実際に1秒伸ばすとリクエスト間隔が延びてCSRFトークンが無効になってしまうのか、PHPフレームワークへのリクエストで失敗してしまいました。試行錯誤の結果、sleep時間を0.1秒(100ミリ秒)ずつ伸ばしていくと各方面丸く収まるということがわかったので現状そのように実装しています……が、そんなの環境によって変わるよねえ。なんという薄氷を踏むような実装。


あくまで「報告」であって推奨できる実装じゃありませんけど、そういう罠もあるんだなあと思ったので。


元はといえば詳細情報をまとめて取って来れない設計になってるAPIとか、やたらと厳しく同時接続数を絞ってるAPIとか、APIとかAPIとかのせいなんですけど、それは言っても仕方がないので、もしまた動かなくなったら今度こそ「Vueでループ回して1件ずつAPIリクエスト送るようなクソ実装はダメだぞ」ということで改修したいと思います。

これ、当時Vuejsに不慣れでのちのキャリアのためか実験的に実装をやってた前任者の負の遺産なんだよなあ、、実は。素直にPHPフレームワークでまとめて取ってきて描画だけループ回してほしいよ。とほほ。
(今回は時間がないのでそこまでやらない)